mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-27 18:38:52 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac623b7e02 | ||
|
|
fa059363c7 | ||
|
|
dae26ad484 | ||
|
|
03064f1f29 | ||
|
|
faf929acce | ||
|
|
2015d430f4 | ||
|
|
6efddccc6f | ||
|
|
c4eafc595d | ||
|
|
90cdb17275 | ||
|
|
df5aae4ded | ||
|
|
cdf063a35d | ||
|
|
d5d4b7fc1d | ||
|
|
86e25a439e | ||
|
|
09323167db | ||
|
|
a35fbec7ff | ||
|
|
11ca3b59bc | ||
|
|
cfd2165b26 | ||
|
|
6acf8d6404 | ||
|
|
ece220a71d | ||
|
|
8adc72ad57 | ||
|
|
9addf86aa5 | ||
|
|
2913d50a52 | ||
|
|
c6c06b3354 | ||
|
|
8242fa883e | ||
|
|
6646bae26c |
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"repoOwner": "prowler-cloud",
|
||||
"repoName": "prowler",
|
||||
"targetPRLabels": [
|
||||
"backport"
|
||||
],
|
||||
"sourcePRLabels": [
|
||||
"was-backported"
|
||||
],
|
||||
"copySourcePRLabels": false,
|
||||
"copySourcePRReviewers": true,
|
||||
"prTitle": "{{sourcePullRequest.title}}",
|
||||
"commitConflicts": true
|
||||
}
|
||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -14,7 +14,6 @@ Please include a summary of the change and which issue is fixed. List any depend
|
||||
- If so, do we need to update permissions for the provider? Please review this carefully.
|
||||
- [ ] Review if the code is being covered by tests.
|
||||
- [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
|
||||
- [ ] Review if backport is needed.
|
||||
|
||||
### License
|
||||
|
||||
|
||||
42
.github/workflows/backport.yml
vendored
42
.github/workflows/backport.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Automatic Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: ['master']
|
||||
types: ['labeled', 'closed']
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport PR
|
||||
if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
# Workaround not to fail the workflow if the PR does not need a backport
|
||||
# https://github.com/sorenlouv/backport-github-action/issues/127#issuecomment-2258561266
|
||||
- name: Check for backport labels
|
||||
id: check_labels
|
||||
run: |-
|
||||
labels='${{ toJSON(github.event.pull_request.labels.*.name) }}'
|
||||
echo "$labels"
|
||||
matched=$(echo "${labels}" | jq '. | map(select(startswith("backport-to-"))) | length')
|
||||
echo "matched=$matched"
|
||||
echo "matched=$matched" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Backport Action
|
||||
if: fromJSON(steps.check_labels.outputs.matched) > 0
|
||||
uses: sorenlouv/backport-github-action@v9.5.1
|
||||
with:
|
||||
github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
auto_backport_label_prefix: backport-to-
|
||||
|
||||
- name: Info log
|
||||
if: ${{ success() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
|
||||
run: cat ~/.backport/backport.info.log
|
||||
|
||||
- name: Debug log
|
||||
if: ${{ failure() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
|
||||
run: cat ~/.backport/backport.debug.log
|
||||
@@ -16,9 +16,9 @@ jobs:
|
||||
name: Documentation Link
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Leave PR comment with the Prowler Documentation URI
|
||||
- name: Leave PR comment with the SaaS Documentation URI
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
body: |
|
||||
You can check the documentation for this PR here -> [Prowler Documentation](https://prowler-prowler-docs--${{ env.PR_NUMBER }}.com.readthedocs.build/projects/prowler-open-source/en/${{ env.PR_NUMBER }}/)
|
||||
You can check the documentation for this PR here -> [SaaS Documentation](https://prowler-prowler-docs--${{ env.PR_NUMBER }}.com.readthedocs.build/projects/prowler-open-source/en/${{ env.PR_NUMBER }}/)
|
||||
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
run: |
|
||||
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--data '{"event_type":"dispatch","client_payload":{"version":"v3-latest", "tag": "${{ env.LATEST_COMMIT_HASH }}"}}'
|
||||
|
||||
@@ -162,6 +162,6 @@ jobs:
|
||||
run: |
|
||||
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--data '{"event_type":"dispatch","client_payload":{"version":"release", "tag":"${{ needs.container-build-push.outputs.prowler_version }}"}}'
|
||||
|
||||
2
.github/workflows/find-secrets.yml
vendored
2
.github/workflows/find-secrets.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.82.6
|
||||
uses: trufflesecurity/trufflehog@3.80.4
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
1
.github/workflows/labeler.yml
vendored
1
.github/workflows/labeler.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
|
||||
3
.github/workflows/pull-request.yml
vendored
3
.github/workflows/pull-request.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@v45
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: ./**
|
||||
files_ignore: |
|
||||
@@ -31,7 +31,6 @@ jobs:
|
||||
docs/**
|
||||
permissions/**
|
||||
mkdocs.yml
|
||||
.backportrc.json
|
||||
- name: Install poetry
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
|
||||
10
.github/workflows/pypi-release.yml
vendored
10
.github/workflows/pypi-release.yml
vendored
@@ -8,6 +8,8 @@ env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
PYTHON_VERSION: 3.11
|
||||
CACHE: "poetry"
|
||||
# TODO: create a bot user for this kind of tasks, like prowler-bot
|
||||
GIT_COMMITTER_EMAIL: "sergio@prowler.com"
|
||||
|
||||
jobs:
|
||||
release-prowler-job:
|
||||
@@ -45,6 +47,14 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: ${{ env.CACHE }}
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Build Prowler package
|
||||
run: |
|
||||
poetry build
|
||||
|
||||
@@ -50,13 +50,13 @@ jobs:
|
||||
|
||||
# Create pull request
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
commit-message: "feat(regions_update): Update regions for AWS services"
|
||||
token: ${{ secrets.PROWLER_ACCESS_TOKEN }}
|
||||
commit-message: "feat(regions_update): Update regions for AWS services."
|
||||
branch: "aws-services-regions-updated-${{ github.sha }}"
|
||||
labels: "status/waiting-for-revision, severity/low, provider/aws, backport-to-v3"
|
||||
title: "chore(regions_update): Changes in regions for AWS services"
|
||||
labels: "status/waiting-for-revision, severity/low, provider/aws, backport-v3"
|
||||
title: "chore(regions_update): Changes in regions for AWS services."
|
||||
body: |
|
||||
### Description
|
||||
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -2,9 +2,9 @@ FROM python:3.12-alpine
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
# Update system dependencies and install essential tools
|
||||
# Update system dependencies
|
||||
#hadolint ignore=DL3018
|
||||
RUN apk --no-cache upgrade && apk --no-cache add curl git
|
||||
RUN apk --no-cache upgrade && apk --no-cache add curl
|
||||
|
||||
# Create nonroot user
|
||||
RUN mkdir -p /home/prowler && \
|
||||
@@ -13,17 +13,18 @@ RUN mkdir -p /home/prowler && \
|
||||
chown -R prowler:prowler /home/prowler
|
||||
USER prowler
|
||||
|
||||
# Copy necessary files
|
||||
# Copy necessary files
|
||||
WORKDIR /home/prowler
|
||||
COPY prowler/ /home/prowler/prowler/
|
||||
COPY dashboard/ /home/prowler/dashboard/
|
||||
COPY pyproject.toml /home/prowler
|
||||
COPY README.md /home/prowler
|
||||
|
||||
# Install Python dependencies
|
||||
# Install dependencies
|
||||
ENV HOME='/home/prowler'
|
||||
ENV PATH="$HOME/.local/bin:$PATH"
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
#hadolint ignore=DL3013
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir .
|
||||
|
||||
# Remove deprecated dash dependencies
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p align="center">
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog"><img width="30" height="30" alt="Prowler community on Slack" src="https://github.com/prowler-cloud/prowler/assets/38561120/3c8b4ec5-6849-41a5-b5e1-52bbb94af73a"></a>
|
||||
<br>
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-2oinmgmw6-cl7gOrljSEqo_aoripVPFA">Join our Prowler community!</a>
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog">Join our Prowler community!</a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center">
|
||||
@@ -63,9 +63,9 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 457 | 67 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
|
||||
| GCP | 77 | 13 -> `prowler gcp --list-services` | 2 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
|
||||
| Azure | 136 | 17 -> `prowler azure --list-services` | 3 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| AWS | 385 | 67 -> `prowler aws --list-services` | 28 -> `prowler aws --list-compliance` | 7 -> `prowler aws --list-categories` |
|
||||
| GCP | 77 | 13 -> `prowler gcp --list-services` | 1 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
|
||||
| Azure | 135 | 16 -> `prowler azure --list-services` | 2 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
|
||||
|
||||
# 💻 Installation
|
||||
|
||||
@@ -2223,232 +2223,3 @@ def get_section_containers_ens(data, section_1, section_2, section_3, section_4)
|
||||
section_containers.append(section_container)
|
||||
|
||||
return html.Div(section_containers, className="compliance-data-layout")
|
||||
|
||||
|
||||
# This function extracts and compares up to two numeric values, ensuring correct sorting for version-like strings.
|
||||
def extract_numeric_values(value):
|
||||
numbers = re.findall(r"\d+", str(value))
|
||||
if len(numbers) >= 2:
|
||||
return int(numbers[0]), int(numbers[1])
|
||||
elif len(numbers) == 1:
|
||||
return int(numbers[0]), 0
|
||||
return 0, 0
|
||||
|
||||
|
||||
def get_section_containers_kisa_ismsp(data, section_1, section_2):
|
||||
data["STATUS"] = data["STATUS"].apply(map_status_to_icon)
|
||||
data[section_1] = data[section_1].astype(str)
|
||||
data[section_2] = data[section_2].astype(str)
|
||||
data.sort_values(
|
||||
by=section_1,
|
||||
key=lambda x: x.map(extract_numeric_values),
|
||||
ascending=True,
|
||||
inplace=True,
|
||||
)
|
||||
|
||||
findings_counts_section = (
|
||||
data.groupby([section_2, "STATUS"]).size().unstack(fill_value=0)
|
||||
)
|
||||
findings_counts_name = (
|
||||
data.groupby([section_1, "STATUS"]).size().unstack(fill_value=0)
|
||||
)
|
||||
|
||||
section_containers = []
|
||||
|
||||
for name in data[section_1].unique():
|
||||
success_name = (
|
||||
findings_counts_name.loc[name, pass_emoji]
|
||||
if pass_emoji in findings_counts_name.columns
|
||||
else 0
|
||||
)
|
||||
failed_name = (
|
||||
findings_counts_name.loc[name, fail_emoji]
|
||||
if fail_emoji in findings_counts_name.columns
|
||||
else 0
|
||||
)
|
||||
|
||||
fig_name = go.Figure(
|
||||
data=[
|
||||
go.Bar(
|
||||
name="Failed",
|
||||
x=[failed_name],
|
||||
y=[""],
|
||||
orientation="h",
|
||||
marker=dict(color="#e77676"),
|
||||
width=[0.8],
|
||||
),
|
||||
go.Bar(
|
||||
name="Success",
|
||||
x=[success_name],
|
||||
y=[""],
|
||||
orientation="h",
|
||||
marker=dict(color="#45cc6e"),
|
||||
width=[0.8],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
fig_name.update_layout(
|
||||
barmode="stack",
|
||||
margin=dict(l=10, r=10, t=10, b=10),
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
showlegend=False,
|
||||
width=350,
|
||||
height=30,
|
||||
xaxis=dict(showticklabels=False, showgrid=False, zeroline=False),
|
||||
yaxis=dict(showticklabels=False, showgrid=False, zeroline=False),
|
||||
annotations=[
|
||||
dict(
|
||||
x=success_name + failed_name,
|
||||
y=0,
|
||||
xref="x",
|
||||
yref="y",
|
||||
text=str(success_name),
|
||||
showarrow=False,
|
||||
font=dict(color="#45cc6e", size=14),
|
||||
xanchor="left",
|
||||
yanchor="middle",
|
||||
),
|
||||
dict(
|
||||
x=0,
|
||||
y=0,
|
||||
xref="x",
|
||||
yref="y",
|
||||
text=str(failed_name),
|
||||
showarrow=False,
|
||||
font=dict(color="#e77676", size=14),
|
||||
xanchor="right",
|
||||
yanchor="middle",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
graph_name = dcc.Graph(
|
||||
figure=fig_name, config={"staticPlot": True}, className="info-bar"
|
||||
)
|
||||
|
||||
graph_div = html.Div(graph_name, className="graph-section")
|
||||
|
||||
direct_internal_items = []
|
||||
|
||||
for section in data[data[section_1] == name][section_2].unique():
|
||||
specific_data = data[
|
||||
(data[section_1] == name) & (data[section_2] == section)
|
||||
]
|
||||
success_section = (
|
||||
findings_counts_section.loc[section, pass_emoji]
|
||||
if pass_emoji in findings_counts_section.columns
|
||||
else 0
|
||||
)
|
||||
failed_section = (
|
||||
findings_counts_section.loc[section, fail_emoji]
|
||||
if fail_emoji in findings_counts_section.columns
|
||||
else 0
|
||||
)
|
||||
|
||||
data_table = dash_table.DataTable(
|
||||
data=specific_data.to_dict("records"),
|
||||
columns=[
|
||||
{"name": i, "id": i}
|
||||
for i in ["CHECKID", "STATUS", "REGION", "ACCOUNTID", "RESOURCEID"]
|
||||
],
|
||||
style_table={"overflowX": "auto"},
|
||||
style_as_list_view=True,
|
||||
style_cell={"textAlign": "left", "padding": "5px"},
|
||||
)
|
||||
|
||||
fig_section = go.Figure(
|
||||
data=[
|
||||
go.Bar(
|
||||
name="Failed",
|
||||
x=[failed_section],
|
||||
y=[""],
|
||||
orientation="h",
|
||||
marker=dict(color="#e77676"),
|
||||
),
|
||||
go.Bar(
|
||||
name="Success",
|
||||
x=[success_section],
|
||||
y=[""],
|
||||
orientation="h",
|
||||
marker=dict(color="#45cc6e"),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
fig_section.update_layout(
|
||||
barmode="stack",
|
||||
margin=dict(l=10, r=10, t=10, b=10),
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
showlegend=False,
|
||||
width=350,
|
||||
height=30,
|
||||
xaxis=dict(showticklabels=False, showgrid=False, zeroline=False),
|
||||
yaxis=dict(showticklabels=False, showgrid=False, zeroline=False),
|
||||
annotations=[
|
||||
dict(
|
||||
x=success_section + failed_section,
|
||||
y=0,
|
||||
xref="x",
|
||||
yref="y",
|
||||
text=str(success_section),
|
||||
showarrow=False,
|
||||
font=dict(color="#45cc6e", size=14),
|
||||
xanchor="left",
|
||||
yanchor="middle",
|
||||
),
|
||||
dict(
|
||||
x=0,
|
||||
y=0,
|
||||
xref="x",
|
||||
yref="y",
|
||||
text=str(failed_section),
|
||||
showarrow=False,
|
||||
font=dict(color="#e77676", size=14),
|
||||
xanchor="right",
|
||||
yanchor="middle",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
graph_section = dcc.Graph(
|
||||
figure=fig_section,
|
||||
config={"staticPlot": True},
|
||||
className="info-bar-child",
|
||||
)
|
||||
|
||||
graph_div_section = html.Div(graph_section, className="graph-section-req")
|
||||
|
||||
internal_accordion_item = dbc.AccordionItem(
|
||||
title=section,
|
||||
children=[html.Div([data_table], className="inner-accordion-content")],
|
||||
)
|
||||
|
||||
internal_section_container = html.Div(
|
||||
[
|
||||
graph_div_section,
|
||||
dbc.Accordion(
|
||||
[internal_accordion_item], start_collapsed=True, flush=True
|
||||
),
|
||||
],
|
||||
className="accordion-inner--child",
|
||||
)
|
||||
|
||||
direct_internal_items.append(internal_section_container)
|
||||
|
||||
accordion_item = dbc.AccordionItem(
|
||||
title=f"{name}", children=direct_internal_items
|
||||
)
|
||||
section_container = html.Div(
|
||||
[
|
||||
graph_div,
|
||||
dbc.Accordion([accordion_item], start_collapsed=True, flush=True),
|
||||
],
|
||||
className="accordion-inner",
|
||||
)
|
||||
|
||||
section_containers.append(section_container)
|
||||
|
||||
return html.Div(section_containers, className="compliance-data-layout")
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_kisa_ismsp
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBDOMAIN",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
# "REQUIREMENTS_DESCRIPTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_kisa_ismsp(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SUBDOMAIN", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_kisa_ismsp
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBDOMAIN",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
# "REQUIREMENTS_DESCRIPTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_kisa_ismsp(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SUBDOMAIN", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -222,7 +222,7 @@ class ec2_securitygroup_with_many_ingress_egress_rules(Check):
|
||||
max_security_group_rules = ec2_client.audit_config.get(
|
||||
"max_security_group_rules", 50
|
||||
)
|
||||
for security_group_arn, security_group in ec2_client.security_groups.items():
|
||||
for security_group in ec2_client.security_groups:
|
||||
```
|
||||
|
||||
```yaml title="config.yaml"
|
||||
@@ -272,7 +272,7 @@ Each Prowler check has metadata associated which is stored at the same level of
|
||||
# Severity holds the check's severity, always in lowercase (critical, high, medium, low or informational)
|
||||
"Severity": "critical",
|
||||
# ResourceType only for AWS, holds the type from here
|
||||
# https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html
|
||||
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
|
||||
"ResourceType": "Other",
|
||||
# Description holds the title of the check, for now is the same as CheckTitle
|
||||
"Description": "Ensure there are no EC2 AMIs set as Public.",
|
||||
|
||||
@@ -14,8 +14,10 @@ Once that is satisfied go ahead and clone your forked repo:
|
||||
git clone https://github.com/<your-github-user>/prowler
|
||||
cd prowler
|
||||
```
|
||||
For isolation and to avoid conflicts with other environments, we recommend using `poetry`, a Python dependency management tool. You can install it by following the instructions [here](https://python-poetry.org/docs/#installation).
|
||||
|
||||
For isolation and avoid conflicts with other environments, we recommend usage of `poetry`:
|
||||
```
|
||||
pip install poetry
|
||||
```
|
||||
Then install all dependencies including the ones for developers:
|
||||
```
|
||||
poetry install --with dev
|
||||
@@ -48,8 +50,6 @@ You can see all dependencies in file `pyproject.toml`.
|
||||
|
||||
Moreover, you would need to install [`TruffleHog`](https://github.com/trufflesecurity/trufflehog) on the latest version to check for secrets in the code. You can install it using the official installation guide [here](https://github.com/trufflesecurity/trufflehog?tab=readme-ov-file#floppy_disk-installation).
|
||||
|
||||
Additionally, please ensure to follow the code documentation practices outlined in this guide: [Google Python Style Guide - Comments and Docstrings](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings).
|
||||
|
||||
???+ note
|
||||
If you have any trouble when committing to the Prowler repository, add the `--no-verify` flag to the `git commit` command.
|
||||
|
||||
|
||||
@@ -592,7 +592,7 @@ is following the actual format, add one function where the client is passed to b
|
||||
`mock_api_<endpoint>_calls` (*endpoint* refers to the first attribute pointed after *client*).
|
||||
|
||||
In the example of BigQuery the function is called `mock_api_dataset_calls`. And inside of this function we found an assignation to
|
||||
be used in the `_get_datasets` method in BigQuery class:
|
||||
be used in the `__get_datasets__` method in BigQuery class:
|
||||
|
||||
```python
|
||||
# Mocking datasets
|
||||
@@ -765,7 +765,7 @@ from tests.providers.azure.azure_fixtures import (
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
# Function to mock the service function _get_components, this function task is to return a possible value that real function could returns
|
||||
# Function to mock the service function __get_components__, this function task is to return a possible value that real function could returns
|
||||
def mock_appinsights_get_components(_):
|
||||
return {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
@@ -779,12 +779,12 @@ def mock_appinsights_get_components(_):
|
||||
|
||||
# Patch decorator to use the mocked function instead the function with the real API call
|
||||
@patch(
|
||||
"prowler.providers.azure.services.appinsights.appinsights_service.AppInsights._get_components",
|
||||
"prowler.providers.azure.services.appinsights.appinsights_service.AppInsights.__get_components__",
|
||||
new=mock_appinsights_get_components,
|
||||
)
|
||||
class Test_AppInsights_Service:
|
||||
# Mandatory test for every service, this method test the instance of the client is correct
|
||||
def test_get_client(self):
|
||||
def test__get_client__(self):
|
||||
app_insights = AppInsights(set_mocked_azure_provider())
|
||||
assert (
|
||||
app_insights.clients[AZURE_SUBSCRIPTION_ID].__class__.__name__
|
||||
@@ -794,8 +794,8 @@ class Test_AppInsights_Service:
|
||||
def test__get_subscriptions__(self):
|
||||
app_insights = AppInsights(set_mocked_azure_provider())
|
||||
assert app_insights.subscriptions.__class__.__name__ == "dict"
|
||||
# Test for the function _get_components, inside this client is used the mocked function
|
||||
def test_get_components(self):
|
||||
# Test for the function __get_components__, inside this client is used the mocked function
|
||||
def test__get_components__(self):
|
||||
appinsights = AppInsights(set_mocked_azure_provider())
|
||||
assert len(appinsights.components) == 1
|
||||
assert (
|
||||
|
||||
109
docs/index.md
109
docs/index.md
@@ -19,40 +19,14 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
## Quick Start
|
||||
### Installation
|
||||
|
||||
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), thus can be installed as Python package with `Python >= 3.9`:
|
||||
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), thus can be installed using pip with `Python >= 3.9`:
|
||||
|
||||
=== "pipx"
|
||||
|
||||
[pipx](https://pipx.pypa.io/stable/) is a tool to install Python applications in isolated environments. It is recommended to use `pipx` for a global installation.
|
||||
=== "Generic"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9`
|
||||
* `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/).
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
To upgrade Prowler to the latest version, run:
|
||||
|
||||
``` bash
|
||||
pipx upgrade prowler
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
???+ warning
|
||||
This method is not recommended because it will modify the environment which you choose to install. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for a global installation.
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9`
|
||||
* `Python pip >= 21.0.0`
|
||||
* `Python pip >= 3.9`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
@@ -62,19 +36,13 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
prowler -v
|
||||
```
|
||||
|
||||
To upgrade Prowler to the latest version, run:
|
||||
|
||||
``` bash
|
||||
pip install --upgrade prowler
|
||||
```
|
||||
|
||||
=== "Docker"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* Have `docker` installed: https://docs.docker.com/get-docker/.
|
||||
* In the command below, change `-v` to your local directory path in order to access the reports.
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
* In the command below, change `-v` to your local directory path in order to access the reports.
|
||||
|
||||
_Commands_:
|
||||
|
||||
@@ -86,21 +54,41 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
--env AWS_SESSION_TOKEN toniblyx/prowler:latest
|
||||
```
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
_Requirements for Ubuntu 20.04.3 LTS_:
|
||||
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
* Install python 3.9 with: `sudo apt-get install python3.9`
|
||||
* Remove python 3.8 to avoid conflicts if you can: `sudo apt-get remove python3.8`
|
||||
* Make sure you have the python3 distutils package installed: `sudo apt-get install python3-distutils`
|
||||
* To make sure you use pip for 3.9 get the get-pip script with: `curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py`
|
||||
* Execute it with the proper python version: `sudo python3.9 get-pip.py`
|
||||
* Now you should have pip for 3.9 ready: `pip3.9 --version`
|
||||
|
||||
_Commands_:
|
||||
|
||||
```
|
||||
pip3.9 install prowler
|
||||
export PATH=$PATH:/home/$HOME/.local/bin/
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "GitHub"
|
||||
|
||||
_Requirements for Developers_:
|
||||
|
||||
* `git`
|
||||
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
* `git`, `Python >= 3.9`, `pip` and `poetry` installed (`pip install poetry`)
|
||||
|
||||
_Commands_:
|
||||
|
||||
```
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler
|
||||
poetry shell
|
||||
poetry install
|
||||
poetry run python prowler.py -v
|
||||
python prowler.py -v
|
||||
```
|
||||
???+ note
|
||||
If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
|
||||
@@ -109,33 +97,15 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
* Latest Amazon Linux 2 should come with Python 3.9 already installed however it may need pip. Install Python pip 3.9 with: `sudo yum install -y python3-pip`.
|
||||
* Make sure setuptools for python is already installed with: `pip3 install setuptools`
|
||||
|
||||
_Commands_:
|
||||
|
||||
```
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9`.
|
||||
* `Python >= 3.9`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
sudo apt update
|
||||
sudo apt install pipx
|
||||
pipx ensurepath
|
||||
pipx install prowler
|
||||
pip3.9 install prowler
|
||||
export PATH=$PATH:/home/$HOME/.local/bin/
|
||||
prowler -v
|
||||
```
|
||||
|
||||
@@ -155,7 +125,7 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
|
||||
=== "AWS CloudShell"
|
||||
|
||||
After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it's already included in AL2023. Prowler can thus be easily installed following the Generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell:
|
||||
After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [2](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it's already included in AL2023. Prowler can thus be easily installed following the Generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell:
|
||||
|
||||
_Requirements_:
|
||||
|
||||
@@ -163,13 +133,11 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
|
||||
_Commands_:
|
||||
|
||||
```bash
|
||||
```
|
||||
sudo bash
|
||||
adduser prowler
|
||||
su prowler
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
pip install prowler
|
||||
cd /tmp
|
||||
prowler aws
|
||||
```
|
||||
@@ -185,12 +153,9 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
|
||||
_Commands_:
|
||||
|
||||
```bash
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
cd /tmp
|
||||
prowler azure --az-cli-auth
|
||||
```
|
||||
pip install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
## Prowler container versions
|
||||
|
||||
@@ -13,53 +13,37 @@ The following list includes all the AWS checks with configurable variables that
|
||||
|
||||
| Check Name | Value | Type |
|
||||
|---------------------------------------------------------------|--------------------------------------------------|-----------------|
|
||||
| `acm_certificates_expiration_check` | `days_to_expire_threshold` | Integer |
|
||||
| `appstream_fleet_maximum_session_duration` | `max_session_duration_seconds` | Integer |
|
||||
| `appstream_fleet_session_disconnect_timeout` | `max_disconnect_timeout_in_seconds` | Integer |
|
||||
| `appstream_fleet_session_idle_disconnect_timeout` | `max_idle_disconnect_timeout_in_seconds` | Integer |
|
||||
| `autoscaling_find_secrets_ec2_launch_configuration` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `awslambda_function_no_secrets_in_code` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `awslambda_function_no_secrets_in_variables` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `awslambda_function_using_supported_runtimes` | `obsolete_lambda_runtimes` | Integer |
|
||||
| `awslambda_function_vpc_is_in_multi_azs` | `lambda_min_azs` | Integer |
|
||||
| `cloudformation_stack_outputs_find_secrets` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_actions` | List of Strings |
|
||||
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_entropy` | Integer |
|
||||
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_minutes` | Integer |
|
||||
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_actions` | List of Strings |
|
||||
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_entropy` | Integer |
|
||||
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_minutes` | Integer |
|
||||
| `cloudwatch_log_group_no_secrets_in_logs` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `cloudwatch_log_group_retention_policy_specific_days_enabled` | `log_group_retention_days` | Integer |
|
||||
| `codebuild_project_no_secrets_in_variables` | `excluded_sensitive_environment_variables` | List of Strings |
|
||||
| `codebuild_project_no_secrets_in_variables` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `config_recorder_all_regions_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `drs_job_exist` | `mute_non_default_regions` | Boolean |
|
||||
| `ec2_elastic_ip_shodan` | `shodan_api_key` | String |
|
||||
| `ec2_instance_older_than_specific_days` | `max_ec2_instance_age_in_days` | Integer |
|
||||
| `ec2_instance_secrets_user_data` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `ec2_launch_template_no_secrets` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings |
|
||||
| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_interface_types` | List of Strings |
|
||||
| `ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports`| `ec2_sg_high_risk_ports` | List of Integer |
|
||||
| `ec2_securitygroup_with_many_ingress_egress_rules` | `max_security_group_rules` | Integer |
|
||||
| `ecs_task_definitions_no_environment_secrets` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String |
|
||||
| `eks_cluster_uses_a_supported_version` | `eks_cluster_oldest_version_supported` | String |
|
||||
| `eks_control_plane_logging_all_types_enabled` | `eks_required_log_types` | List of Strings |
|
||||
| `elb_is_in_multiple_az` | `elb_min_azs` | Integer |
|
||||
| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer |
|
||||
| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer |
|
||||
| `iam_user_console_access_unused` | `max_console_access_days` | Integer |
|
||||
| `organizations_delegated_administrators` | `organizations_trusted_delegated_administrators` | List of Strings |
|
||||
| `organizations_scp_check_deny_regions` | `organizations_enabled_regions` | List of Strings |
|
||||
| `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean |
|
||||
| `securityhub_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `ssm_document_secrets` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean |
|
||||
| `ec2_elastic_ip_shodan` | `shodan_api_key` | String |
|
||||
| `ec2_securitygroup_with_many_ingress_egress_rules` | `max_security_group_rules` | Integer |
|
||||
| `ec2_instance_older_than_specific_days` | `max_ec2_instance_age_in_days` | Integer |
|
||||
| `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings |
|
||||
| `vpc_endpoint_services_allowed_principals_trust_boundaries` | `trusted_account_ids` | List of Strings |
|
||||
| `cloudwatch_log_group_retention_policy_specific_days_enabled` | `log_group_retention_days` | Integer |
|
||||
| `appstream_fleet_session_idle_disconnect_timeout` | `max_idle_disconnect_timeout_in_seconds` | Integer |
|
||||
| `appstream_fleet_session_disconnect_timeout` | `max_disconnect_timeout_in_seconds` | Integer |
|
||||
| `appstream_fleet_maximum_session_duration` | `max_session_duration_seconds` | Integer |
|
||||
| `awslambda_function_using_supported_runtimes` | `obsolete_lambda_runtimes` | Integer |
|
||||
| `organizations_scp_check_deny_regions` | `organizations_enabled_regions` | List of Strings |
|
||||
| `organizations_delegated_administrators` | `organizations_trusted_delegated_administrators` | List of Strings |
|
||||
| `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String |
|
||||
| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean |
|
||||
| `config_recorder_all_regions_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `drs_job_exist` | `mute_non_default_regions` | Boolean |
|
||||
| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `securityhub_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_entropy` | Integer |
|
||||
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_minutes` | Integer |
|
||||
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_actions` | List of Strings |
|
||||
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_entropy` | Integer |
|
||||
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_minutes` | Integer |
|
||||
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_actions` | List of Strings |
|
||||
| `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean |
|
||||
| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_interface_types` | List of Strings |
|
||||
| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings |
|
||||
| `acm_certificates_expiration_check` | `days_to_expire_threshold` | Integer |
|
||||
| `eks_control_plane_logging_all_types_enabled` | `eks_required_log_types` | List of Strings |
|
||||
|
||||
|
||||
## Azure
|
||||
@@ -141,24 +125,8 @@ aws:
|
||||
[
|
||||
"amazon-elb"
|
||||
]
|
||||
# aws.ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports
|
||||
ec2_sg_high_risk_ports:
|
||||
[
|
||||
25,
|
||||
110,
|
||||
135,
|
||||
143,
|
||||
445,
|
||||
3000,
|
||||
4333,
|
||||
5000,
|
||||
5500,
|
||||
8080,
|
||||
8088,
|
||||
]
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
@@ -400,18 +368,6 @@ aws:
|
||||
"scheduler",
|
||||
]
|
||||
|
||||
# aws.eks_cluster_uses_a_supported_version
|
||||
# EKS clusters must be version 1.28 or higher
|
||||
eks_cluster_oldest_version_supported: "1.28"
|
||||
|
||||
# AWS CodeBuild Configuration
|
||||
# aws.codebuild_project_no_secrets_in_variables
|
||||
# CodeBuild sensitive variables that are excluded from the check
|
||||
excluded_sensitive_environment_variables:
|
||||
[
|
||||
|
||||
]
|
||||
|
||||
# Azure Configuration
|
||||
azure:
|
||||
# Azure Network Configuration
|
||||
|
||||
@@ -10,11 +10,9 @@ prowler dashboard
|
||||
To run Prowler local dashboard with Docker, use:
|
||||
|
||||
```sh
|
||||
docker run -v /your/local/dir/prowler-output:/home/prowler/output --env HOST=0.0.0.0 --publish 127.0.0.1:11666:11666 toniblyx/prowler:latest dashboard
|
||||
docker run --env HOST=0.0.0.0 --publish 127.0.0.1:11666:11666 toniblyx/prowler:latest dashboard
|
||||
```
|
||||
|
||||
Make sure you update the `/your/local/dir/prowler-output` to match the path that contains your prowler output.
|
||||
|
||||
???+ note
|
||||
**Remember that the `dashboard` server is not authenticated, if you expose it to the internet, you are running it at your own risk.**
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ prowler <provider> -c <check_to_fix_1> <check_to_fix_2> ... --fixer
|
||||
```sh
|
||||
prowler <provider> --list-fixers
|
||||
```
|
||||
It's important to note that using the fixers for `Access Analyzer`, `GuardDuty`, and `SecurityHub` may incur additional costs. These AWS services might trigger actions or deploy resources that can lead to charges on your AWS account.
|
||||
|
||||
## Writing a Fixer
|
||||
To write a fixer, you need to create a file called `<check_id>_fixer.py` inside the check folder, with a function called `fixer` that receives either the region or the resource to be fixed as a parameter, and returns a boolean value indicating if the fix was successful or not.
|
||||
|
||||
|
||||
@@ -40,20 +40,20 @@ Mutelist:
|
||||
Regions:
|
||||
- "us-east-1"
|
||||
Resources:
|
||||
- "user-1" # Will mute user-1 in check iam_user_hardware_mfa_enabled
|
||||
- "user-2" # Will mute user-2 in check iam_user_hardware_mfa_enabled
|
||||
- "user-1" # Will ignore user-1 in check iam_user_hardware_mfa_enabled
|
||||
- "user-2" # Will ignore user-2 in check iam_user_hardware_mfa_enabled
|
||||
"ec2_*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*" # Will mute every EC2 check in every account and region
|
||||
- "*" # Will ignore every EC2 check in every account and region
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "test=test" # Will mute every resource containing the string "test" and the tags 'test=test' and
|
||||
- "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and
|
||||
- "project=test|project=stage" # either of ('project=test' OR project=stage) in account 123456789012 and every region
|
||||
"*":
|
||||
Regions:
|
||||
@@ -86,9 +86,9 @@ Mutelist:
|
||||
- "eu-west-1"
|
||||
- "us-east-1"
|
||||
Resources:
|
||||
- "ci-logs" # Will mute bucket "ci-logs" AND ALSO bucket "ci-logs-replica" in specified check and regions
|
||||
- "logs" # Will mute EVERY BUCKET containing the string "logs" in specified check and regions
|
||||
- ".+-logs" # Will mute all buckets containing the terms ci-logs, qa-logs, etc. in specified check and regions
|
||||
- "ci-logs" # Will ignore bucket "ci-logs" AND ALSO bucket "ci-logs-replica" in specified check and regions
|
||||
- "logs" # Will ignore EVERY BUCKET containing the string "logs" in specified check and regions
|
||||
- ".+-logs" # Will ignore all buckets containing the terms ci-logs, qa-logs, etc. in specified check and regions
|
||||
"ecs_task_definitions_no_environment_secrets":
|
||||
Regions:
|
||||
- "*"
|
||||
@@ -99,14 +99,14 @@ Mutelist:
|
||||
- "0123456789012"
|
||||
Regions:
|
||||
- "eu-west-1"
|
||||
- "eu-south-2" # Will mute every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1
|
||||
- "eu-south-2" # Will ignore every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Tags:
|
||||
- "environment=dev" # Will mute every resource containing the tag 'environment=dev' in every account and region
|
||||
- "environment=dev" # Will ignore every resource containing the tag 'environment=dev' in every account and region
|
||||
|
||||
"123456789012":
|
||||
Checks:
|
||||
@@ -119,26 +119,18 @@ Mutelist:
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "environment=prod" # Will mute every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod
|
||||
|
||||
"*":
|
||||
Checks:
|
||||
"ec2_*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "test-resource" # Will mute the resource "test-resource" in all accounts and regions for whatever check from the EC2 service
|
||||
- "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod
|
||||
```
|
||||
|
||||
### Account, Check, Region, Resource, and Tag
|
||||
|
||||
| Field | Description | Logic |
|
||||
|----------|----------|----------|
|
||||
| `account_id` | Use `*` to apply the mutelist to all accounts. | `ANDed` |
|
||||
| `check_name` | The name of the Prowler check. Use `*` to apply the mutelist to all checks, or `service_*` to apply it to all service's checks. | `ANDed` |
|
||||
| `region` | The region identifier. Use `*` to apply the mutelist to all regions. | `ANDed` |
|
||||
| `resource` | The resource identifier. Use `*` to apply the mutelist to all resources. | `ANDed` |
|
||||
| `tag` | The tag value. | `ORed` |
|
||||
| `<account_id>` | Use `*` to apply the mutelist to all accounts. | `ANDed` |
|
||||
| `<check_name>` | The name of the Prowler check. Use `*` to apply the mutelist to all checks. | `ANDed` |
|
||||
| `<region>` | The region identifier. Use `*` to apply the mutelist to all regions. | `ANDed` |
|
||||
| `<resource>` | The resource identifier. Use `*` to apply the mutelist to all resources. | `ANDed` |
|
||||
| `<tag>` | The tag value. | `ORed` |
|
||||
|
||||
|
||||
## How to Use the Mutelist
|
||||
|
||||
@@ -36,11 +36,10 @@ If EBS default encyption is not enabled, sensitive information at rest is not pr
|
||||
|
||||
- `ec2_ebs_default_encryption`
|
||||
|
||||
If your Security groups are not properly configured the attack surface is increased, nonetheless, Prowler will detect those security groups that are being used (they are attached) to only notify those that are being used. This logic applies to the 15 checks related to open ports in security groups, the check for the default security group and for the security groups that allow ingress and egress traffic.
|
||||
If your Security groups are not properly configured the attack surface is increased, nonetheless, Prowler will detect those security groups that are being used (they are attached) to only notify those that are being used. This logic applies to the 15 checks related to open ports in security groups and the check for the default security group.
|
||||
|
||||
- `ec2_securitygroup_allow_ingress_from_internet_to_port_X` (15 checks)
|
||||
- `ec2_securitygroup_default_restrict_traffic`
|
||||
- `ec2_securitygroup_allow_wide_open_public_ipv4`
|
||||
|
||||
Prowler will also check for used Network ACLs to only alerts those with open ports that are being used.
|
||||
|
||||
|
||||
2670
poetry.lock
generated
2670
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,8 @@ from prowler.config.config import (
|
||||
)
|
||||
from prowler.lib.banner import print_banner
|
||||
from prowler.lib.check.check import (
|
||||
bulk_load_checks_metadata,
|
||||
bulk_load_compliance_frameworks,
|
||||
exclude_checks_to_run,
|
||||
exclude_services_to_run,
|
||||
execute_checks,
|
||||
@@ -34,12 +36,10 @@ from prowler.lib.check.check import (
|
||||
)
|
||||
from prowler.lib.check.checks_loader import load_checks_to_execute
|
||||
from prowler.lib.check.compliance import update_checks_metadata_with_compliance
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.custom_checks_metadata import (
|
||||
parse_custom_checks_metadata_file,
|
||||
update_checks_metadata,
|
||||
)
|
||||
from prowler.lib.check.models import CheckMetadata
|
||||
from prowler.lib.cli.parser import ProwlerArgumentParser
|
||||
from prowler.lib.logger import logger, set_logging_config
|
||||
from prowler.lib.outputs.asff.asff import ASFF
|
||||
@@ -54,7 +54,6 @@ from prowler.lib.outputs.compliance.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
|
||||
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
|
||||
from prowler.lib.outputs.compliance.iso27001.iso27001_aws import AWSISO27001
|
||||
from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp_aws import AWSKISAISMSP
|
||||
from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_aws import AWSMitreAttack
|
||||
from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_azure import (
|
||||
AzureMitreAttack,
|
||||
@@ -69,12 +68,8 @@ from prowler.lib.outputs.slack.slack import Slack
|
||||
from prowler.lib.outputs.summary_table import display_summary_table
|
||||
from prowler.providers.aws.lib.s3.s3 import S3
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
|
||||
from prowler.providers.aws.models import AWSOutputOptions
|
||||
from prowler.providers.azure.models import AzureOutputOptions
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
from prowler.providers.gcp.models import GCPOutputOptions
|
||||
from prowler.providers.kubernetes.models import KubernetesOutputOptions
|
||||
|
||||
|
||||
def prowler():
|
||||
@@ -136,7 +131,7 @@ def prowler():
|
||||
|
||||
# Load checks metadata
|
||||
logger.debug("Loading checks metadata from .metadata.json files")
|
||||
bulk_checks_metadata = CheckMetadata.get_bulk(provider)
|
||||
bulk_checks_metadata = bulk_load_checks_metadata(provider)
|
||||
|
||||
if args.list_categories:
|
||||
print_categories(list_categories(bulk_checks_metadata))
|
||||
@@ -146,7 +141,7 @@ def prowler():
|
||||
# Load compliance frameworks
|
||||
logger.debug("Loading compliance frameworks from .json files")
|
||||
|
||||
bulk_compliance_frameworks = Compliance.get_bulk(provider)
|
||||
bulk_compliance_frameworks = bulk_load_compliance_frameworks(provider)
|
||||
# Complete checks metadata with the compliance framework specification
|
||||
bulk_checks_metadata = update_checks_metadata_with_compliance(
|
||||
bulk_compliance_frameworks, bulk_checks_metadata
|
||||
@@ -195,7 +190,7 @@ def prowler():
|
||||
sys.exit()
|
||||
|
||||
# Provider to scan
|
||||
Provider.init_global_provider(args)
|
||||
Provider.set_global_provider(args)
|
||||
global_provider = Provider.get_global_provider()
|
||||
|
||||
# Print Provider Credentials
|
||||
@@ -229,8 +224,7 @@ def prowler():
|
||||
# Once the provider is set and we have the eventual checks based on the resource identifier,
|
||||
# it is time to check what Prowler's checks are going to be executed
|
||||
checks_from_resources = global_provider.get_checks_to_execute_by_audit_resources()
|
||||
# Intersect checks from resources with checks to execute so we only run the checks that apply to the resources with the specified ARNs or tags
|
||||
if getattr(args, "resource_arn", None) or getattr(args, "resource_tag", None):
|
||||
if checks_from_resources:
|
||||
checks_to_execute = checks_to_execute.intersection(checks_from_resources)
|
||||
|
||||
# Sort final check list
|
||||
@@ -240,22 +234,7 @@ def prowler():
|
||||
global_provider.mutelist = args.mutelist_file
|
||||
|
||||
# Setup Output Options
|
||||
if provider == "aws":
|
||||
output_options = AWSOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "azure":
|
||||
output_options = AzureOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "gcp":
|
||||
output_options = GCPOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "kubernetes":
|
||||
output_options = KubernetesOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
global_provider.output_options = (args, bulk_checks_metadata)
|
||||
|
||||
# Run the quick inventory for the provider if available
|
||||
if hasattr(args, "quick_inventory") and args.quick_inventory:
|
||||
@@ -271,7 +250,6 @@ def prowler():
|
||||
global_provider,
|
||||
custom_checks_metadata,
|
||||
args.config_file,
|
||||
output_options,
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
@@ -279,7 +257,7 @@ def prowler():
|
||||
)
|
||||
|
||||
# Prowler Fixer
|
||||
if output_options.fixer:
|
||||
if global_provider.output_options.fixer:
|
||||
print(f"{Style.BRIGHT}\nRunning Prowler Fixer, please wait...{Style.RESET_ALL}")
|
||||
# Check if there are any FAIL findings
|
||||
if any("FAIL" in finding.status for finding in findings):
|
||||
@@ -325,8 +303,7 @@ def prowler():
|
||||
# TODO: this part is needed since the checks generates a Check_Report_XXX and the output uses Finding
|
||||
# This will be refactored for the outputs generate directly the Finding
|
||||
finding_outputs = [
|
||||
Finding.generate_output(global_provider, finding, output_options)
|
||||
for finding in findings
|
||||
Finding.generate_output(global_provider, finding) for finding in findings
|
||||
]
|
||||
|
||||
generated_outputs = {"regular": [], "compliance": []}
|
||||
@@ -334,8 +311,8 @@ def prowler():
|
||||
if args.output_formats:
|
||||
for mode in args.output_formats:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/"
|
||||
f"{output_options.output_filename}"
|
||||
f"{global_provider.output_options.output_directory}/"
|
||||
f"{global_provider.output_options.output_filename}"
|
||||
)
|
||||
if mode == "csv":
|
||||
csv_output = CSV(
|
||||
@@ -377,16 +354,16 @@ def prowler():
|
||||
)
|
||||
|
||||
# Compliance Frameworks
|
||||
input_compliance_frameworks = set(output_options.output_modes).intersection(
|
||||
get_available_compliance_frameworks(provider)
|
||||
)
|
||||
input_compliance_frameworks = set(
|
||||
global_provider.output_options.output_modes
|
||||
).intersection(get_available_compliance_frameworks(provider))
|
||||
if provider == "aws":
|
||||
for compliance_name in input_compliance_frameworks:
|
||||
if compliance_name.startswith("cis_"):
|
||||
# Generate CIS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
cis = AWSCIS(
|
||||
findings=finding_outputs,
|
||||
@@ -399,8 +376,8 @@ def prowler():
|
||||
elif compliance_name == "mitre_attack_aws":
|
||||
# Generate MITRE ATT&CK Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
mitre_attack = AWSMitreAttack(
|
||||
findings=finding_outputs,
|
||||
@@ -413,8 +390,8 @@ def prowler():
|
||||
elif compliance_name.startswith("ens_"):
|
||||
# Generate ENS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
ens = AWSENS(
|
||||
findings=finding_outputs,
|
||||
@@ -427,8 +404,8 @@ def prowler():
|
||||
elif compliance_name.startswith("aws_well_architected_framework"):
|
||||
# Generate AWS Well-Architected Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
aws_well_architected = AWSWellArchitected(
|
||||
findings=finding_outputs,
|
||||
@@ -441,8 +418,8 @@ def prowler():
|
||||
elif compliance_name.startswith("iso27001_"):
|
||||
# Generate ISO27001 Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
iso27001 = AWSISO27001(
|
||||
findings=finding_outputs,
|
||||
@@ -452,24 +429,10 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(iso27001)
|
||||
iso27001.batch_write_data_to_file()
|
||||
elif compliance_name.startswith("kisa"):
|
||||
# Generate KISA-ISMS-P Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
kisa_ismsp = AWSKISAISMSP(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(kisa_ismsp)
|
||||
kisa_ismsp.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
@@ -485,8 +448,8 @@ def prowler():
|
||||
if compliance_name.startswith("cis_"):
|
||||
# Generate CIS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
cis = AzureCIS(
|
||||
findings=finding_outputs,
|
||||
@@ -499,8 +462,8 @@ def prowler():
|
||||
elif compliance_name == "mitre_attack_azure":
|
||||
# Generate MITRE ATT&CK Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
mitre_attack = AzureMitreAttack(
|
||||
findings=finding_outputs,
|
||||
@@ -512,8 +475,8 @@ def prowler():
|
||||
mitre_attack.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
@@ -529,8 +492,8 @@ def prowler():
|
||||
if compliance_name.startswith("cis_"):
|
||||
# Generate CIS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
cis = GCPCIS(
|
||||
findings=finding_outputs,
|
||||
@@ -543,8 +506,8 @@ def prowler():
|
||||
elif compliance_name == "mitre_attack_gcp":
|
||||
# Generate MITRE ATT&CK Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
mitre_attack = GCPMitreAttack(
|
||||
findings=finding_outputs,
|
||||
@@ -556,8 +519,8 @@ def prowler():
|
||||
mitre_attack.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
@@ -573,8 +536,8 @@ def prowler():
|
||||
if compliance_name.startswith("cis_"):
|
||||
# Generate CIS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
cis = KubernetesCIS(
|
||||
findings=finding_outputs,
|
||||
@@ -586,8 +549,8 @@ def prowler():
|
||||
cis.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
f"{global_provider.output_options.output_directory}/compliance/"
|
||||
f"{global_provider.output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
@@ -630,7 +593,7 @@ def prowler():
|
||||
aws_partition=global_provider.identity.partition,
|
||||
aws_session=global_provider.session.current_session,
|
||||
findings=asff_output.data,
|
||||
send_only_fails=output_options.send_sh_only_fails,
|
||||
send_only_fails=global_provider.output_options.send_sh_only_fails,
|
||||
aws_security_hub_available_regions=security_hub_regions,
|
||||
)
|
||||
# Send the findings to Security Hub
|
||||
@@ -656,7 +619,7 @@ def prowler():
|
||||
display_summary_table(
|
||||
findings,
|
||||
global_provider,
|
||||
output_options,
|
||||
global_provider.output_options,
|
||||
)
|
||||
# Only display compliance table if there are findings (not all MANUAL) and it is a default execution
|
||||
if (
|
||||
@@ -675,13 +638,13 @@ def prowler():
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance,
|
||||
output_options.output_filename,
|
||||
output_options.output_directory,
|
||||
global_provider.output_options.output_filename,
|
||||
global_provider.output_options.output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
if compliance_overview:
|
||||
print(
|
||||
f"\nDetailed compliance results are in {Fore.YELLOW}{output_options.output_directory}/compliance/{Style.RESET_ALL}\n"
|
||||
f"\nDetailed compliance results are in {Fore.YELLOW}{global_provider.output_options.output_directory}/compliance/{Style.RESET_ALL}\n"
|
||||
)
|
||||
|
||||
# If custom checks were passed, remove the modules
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -19,11 +19,8 @@ Mutelist:
|
||||
- "StackSet-AWSControlTowerSecurityResources-*"
|
||||
- "StackSet-AWSControlTowerLoggingResources-*"
|
||||
- "StackSet-AWSControlTowerExecutionRole-*"
|
||||
- "AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER*"
|
||||
- "AWSControlTowerBP-BASELINE-CONFIG-MASTER*"
|
||||
- "StackSet-AWSControlTower*"
|
||||
- "CLOUDTRAIL-ENABLED-ON-SHARED-ACCOUNTS-*"
|
||||
- "AFT-Backend*"
|
||||
- "AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER"
|
||||
- "AWSControlTowerBP-BASELINE-CONFIG-MASTER"
|
||||
"cloudtrail_*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -11,7 +11,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "4.4.1"
|
||||
prowler_version = "4.3.4"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -41,24 +41,8 @@ aws:
|
||||
[
|
||||
"amazon-elb"
|
||||
]
|
||||
# aws.ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports
|
||||
ec2_sg_high_risk_ports:
|
||||
[
|
||||
25,
|
||||
110,
|
||||
135,
|
||||
143,
|
||||
445,
|
||||
3000,
|
||||
4333,
|
||||
5000,
|
||||
5500,
|
||||
8080,
|
||||
8088,
|
||||
]
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
@@ -102,8 +86,6 @@ aws:
|
||||
"ruby2.5",
|
||||
"ruby2.7",
|
||||
]
|
||||
# aws.awslambda_function_vpc_is_in_multi_azs
|
||||
lambda_min_azs: 2
|
||||
|
||||
# AWS Organizations
|
||||
# aws.organizations_scp_check_deny_regions
|
||||
@@ -289,11 +271,6 @@ aws:
|
||||
# AWS ACM Configuration
|
||||
# aws.acm_certificates_expiration_check
|
||||
days_to_expire_threshold: 7
|
||||
# aws.acm_certificates_rsa_key_length
|
||||
insecure_key_algorithms:
|
||||
[
|
||||
"RSA-1024",
|
||||
]
|
||||
|
||||
# AWS EKS Configuration
|
||||
# aws.eks_control_plane_logging_all_types_enabled
|
||||
@@ -307,33 +284,6 @@ aws:
|
||||
"scheduler",
|
||||
]
|
||||
|
||||
# aws.eks_cluster_uses_a_supported_version
|
||||
# EKS clusters must be version 1.28 or higher
|
||||
eks_cluster_oldest_version_supported: "1.28"
|
||||
|
||||
# AWS CodeBuild Configuration
|
||||
# aws.codebuild_project_no_secrets_in_variables
|
||||
# CodeBuild sensitive variables that are excluded from the check
|
||||
excluded_sensitive_environment_variables:
|
||||
[
|
||||
|
||||
]
|
||||
|
||||
# AWS ELB Configuration
|
||||
# aws.elb_is_in_multiple_az
|
||||
# Minimum number of Availability Zones that an CLB must be in
|
||||
elb_min_azs: 2
|
||||
|
||||
# AWS ELBv2 Configuration
|
||||
# aws.elbv2_is_in_multiple_az
|
||||
# Minimum number of Availability Zones that an ELBv2 must be in
|
||||
elbv2_min_azs: 2
|
||||
|
||||
|
||||
# AWS Secrets Configuration
|
||||
# Patterns to ignore in the secrets checks
|
||||
secrets_ignore_patterns: []
|
||||
|
||||
# Azure Configuration
|
||||
azure:
|
||||
# Azure Network Configuration
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
class ProwlerException(Exception):
|
||||
"""Base exception for all Prowler SDK errors."""
|
||||
|
||||
ERROR_CODES = {
|
||||
(1901, "UnexpectedError"): {
|
||||
"message": "Unexpected error occurred.",
|
||||
"remediation": "Please review the error message and try again.",
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, code, provider=None, file=None, original_exception=None, error_info=None
|
||||
):
|
||||
"""
|
||||
Initialize the ProwlerException class.
|
||||
|
||||
Args:
|
||||
code (int): The error code.
|
||||
provider (str): The provider name.
|
||||
file (str): The file name.
|
||||
original_exception (Exception): The original exception.
|
||||
error_info (dict): The error information.
|
||||
|
||||
Example:
|
||||
A ProwlerException is raised with the following parameters and format:
|
||||
>>> original_exception = Exception("Error occurred.")
|
||||
ProwlerException(1901, "AWS", "file.txt", original_exception)
|
||||
>>> [1901] Unexpected error occurred. - Exception: Error occurred.
|
||||
"""
|
||||
self.code = code
|
||||
self.provider = provider
|
||||
self.file = file
|
||||
if error_info is None:
|
||||
error_info = self.ERROR_CODES.get((code, self.__class__.__name__))
|
||||
self.message = error_info.get("message")
|
||||
self.remediation = error_info.get("remediation")
|
||||
self.original_exception = original_exception
|
||||
# Format -> [code] message - original_exception
|
||||
if original_exception is None:
|
||||
super().__init__(f"[{self.code}] {self.message}")
|
||||
else:
|
||||
super().__init__(
|
||||
f"[{self.code}] {self.message} - {self.original_exception}"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Overriding the __str__ method"""
|
||||
return f"{self.__class__.__name__}[{self.code}]: {self.message} - {self.original_exception}"
|
||||
|
||||
|
||||
class UnexpectedError(ProwlerException):
|
||||
def __init__(self, provider, file, original_exception=None):
|
||||
super().__init__(1901, provider, file, original_exception)
|
||||
@@ -6,6 +6,7 @@ import re
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from pkgutil import walk_packages
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
@@ -14,15 +15,68 @@ from colorama import Fore, Style
|
||||
|
||||
import prowler
|
||||
from prowler.config.config import orange_color
|
||||
from prowler.lib.check.compliance_models import load_compliance_framework
|
||||
from prowler.lib.check.custom_checks_metadata import update_check_metadata
|
||||
from prowler.lib.check.models import Check
|
||||
from prowler.lib.check.utils import recover_checks_from_provider
|
||||
from prowler.lib.check.models import Check, load_check_metadata
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.outputs import report
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
|
||||
# Load all checks metadata
|
||||
def bulk_load_checks_metadata(provider: str) -> dict:
|
||||
bulk_check_metadata = {}
|
||||
checks = recover_checks_from_provider(provider)
|
||||
# Build list of check's metadata files
|
||||
for check_info in checks:
|
||||
# Build check path name
|
||||
check_name = check_info[0]
|
||||
check_path = check_info[1]
|
||||
# Ignore fixer files
|
||||
if check_name.endswith("_fixer"):
|
||||
continue
|
||||
# Append metadata file extension
|
||||
metadata_file = f"{check_path}/{check_name}.metadata.json"
|
||||
# Load metadata
|
||||
check_metadata = load_check_metadata(metadata_file)
|
||||
bulk_check_metadata[check_metadata.CheckID] = check_metadata
|
||||
|
||||
return bulk_check_metadata
|
||||
|
||||
|
||||
# Bulk load all compliance frameworks specification
|
||||
def bulk_load_compliance_frameworks(provider: str) -> dict:
|
||||
"""Bulk load all compliance frameworks specification into a dict"""
|
||||
try:
|
||||
bulk_compliance_frameworks = {}
|
||||
available_compliance_framework_modules = list_compliance_modules()
|
||||
for compliance_framework in available_compliance_framework_modules:
|
||||
if provider in compliance_framework.name:
|
||||
compliance_specification_dir_path = (
|
||||
f"{compliance_framework.module_finder.path}/{provider}"
|
||||
)
|
||||
|
||||
# for compliance_framework in available_compliance_framework_modules:
|
||||
for filename in os.listdir(compliance_specification_dir_path):
|
||||
file_path = os.path.join(
|
||||
compliance_specification_dir_path, filename
|
||||
)
|
||||
# Check if it is a file and ti size is greater than 0
|
||||
if os.path.isfile(file_path) and os.stat(file_path).st_size > 0:
|
||||
# Open Compliance file in JSON
|
||||
# cis_v1.4_aws.json --> cis_v1.4_aws
|
||||
compliance_framework_name = filename.split(".json")[0]
|
||||
# Store the compliance info
|
||||
bulk_compliance_frameworks[compliance_framework_name] = (
|
||||
load_compliance_framework(file_path)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}] -- {e}")
|
||||
|
||||
return bulk_compliance_frameworks
|
||||
|
||||
|
||||
# Exclude checks to run
|
||||
def exclude_checks_to_run(checks_to_execute: set, excluded_checks: list) -> set:
|
||||
for check in excluded_checks:
|
||||
@@ -319,12 +373,100 @@ def parse_checks_from_compliance_framework(
|
||||
return checks_to_execute
|
||||
|
||||
|
||||
def recover_checks_from_provider(
|
||||
provider: str, service: str = None, include_fixers: bool = False
|
||||
) -> list[tuple]:
|
||||
"""
|
||||
Recover all checks from the selected provider and service
|
||||
|
||||
Returns a list of tuples with the following format (check_name, check_path)
|
||||
"""
|
||||
try:
|
||||
checks = []
|
||||
modules = list_modules(provider, service)
|
||||
for module_name in modules:
|
||||
# Format: "prowler.providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
check_module_name = module_name.name
|
||||
# We need to exclude common shared libraries in services
|
||||
if (
|
||||
check_module_name.count(".") == 6
|
||||
and "lib" not in check_module_name
|
||||
and (not check_module_name.endswith("_fixer") or include_fixers)
|
||||
):
|
||||
check_path = module_name.module_finder.path
|
||||
# Check name is the last part of the check_module_name
|
||||
check_name = check_module_name.split(".")[-1]
|
||||
check_info = (check_name, check_path)
|
||||
checks.append(check_info)
|
||||
except ModuleNotFoundError:
|
||||
logger.critical(f"Service {service} was not found for the {provider} provider.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.critical(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
return checks
|
||||
|
||||
|
||||
def list_compliance_modules():
|
||||
"""
|
||||
list_compliance_modules returns the available compliance frameworks and returns their path
|
||||
"""
|
||||
# This module path requires the full path including "prowler."
|
||||
module_path = "prowler.compliance"
|
||||
return walk_packages(
|
||||
importlib.import_module(module_path).__path__,
|
||||
importlib.import_module(module_path).__name__ + ".",
|
||||
)
|
||||
|
||||
|
||||
# List all available modules in the selected provider and service
|
||||
def list_modules(provider: str, service: str):
|
||||
# This module path requires the full path including "prowler."
|
||||
module_path = f"prowler.providers.{provider}.services"
|
||||
if service:
|
||||
module_path += f".{service}"
|
||||
return walk_packages(
|
||||
importlib.import_module(module_path).__path__,
|
||||
importlib.import_module(module_path).__name__ + ".",
|
||||
)
|
||||
|
||||
|
||||
# Import an input check using its path
|
||||
def import_check(check_path: str) -> ModuleType:
|
||||
lib = importlib.import_module(f"{check_path}")
|
||||
return lib
|
||||
|
||||
|
||||
def run_check(check: Check, verbose: bool = False, only_logs: bool = False) -> list:
|
||||
"""
|
||||
Run the check and return the findings
|
||||
Args:
|
||||
check (Check): check class
|
||||
output_options (Any): output options
|
||||
Returns:
|
||||
list: list of findings
|
||||
"""
|
||||
findings = []
|
||||
if verbose:
|
||||
print(
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}"
|
||||
)
|
||||
logger.debug(f"Executing check: {check.CheckID}")
|
||||
try:
|
||||
findings = check.execute()
|
||||
except Exception as error:
|
||||
if not only_logs:
|
||||
print(
|
||||
f"Something went wrong in {check.CheckID}, please use --log-level ERROR"
|
||||
)
|
||||
logger.error(
|
||||
f"{check.CheckID} -- {error.__class__.__name__}[{traceback.extract_tb(error.__traceback__)[-1].lineno}]: {error}"
|
||||
)
|
||||
finally:
|
||||
return findings
|
||||
|
||||
|
||||
def run_fixer(check_findings: list) -> int:
|
||||
"""
|
||||
Run the fixer for the check if it exists and there are any FAIL findings
|
||||
@@ -406,7 +548,6 @@ def execute_checks(
|
||||
global_provider: Any,
|
||||
custom_checks_metadata: Any,
|
||||
config_file: str,
|
||||
output_options: Any,
|
||||
) -> list:
|
||||
# List to store all the check's findings
|
||||
all_findings = []
|
||||
@@ -442,51 +583,22 @@ def execute_checks(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
# Set verbose flag
|
||||
verbose = False
|
||||
if hasattr(output_options, "verbose"):
|
||||
verbose = output_options.verbose
|
||||
elif hasattr(output_options, "fixer"):
|
||||
verbose = output_options.fixer
|
||||
|
||||
# Execution with the --only-logs flag
|
||||
if output_options.only_logs:
|
||||
if global_provider.output_options.only_logs:
|
||||
for check_name in checks_to_execute:
|
||||
# Recover service from check name
|
||||
service = check_name.split("_")[0]
|
||||
try:
|
||||
try:
|
||||
# Import check module
|
||||
check_module_path = f"prowler.providers.{global_provider.type}.services.{service}.{check_name}.{check_name}"
|
||||
lib = import_check(check_module_path)
|
||||
# Recover functions from check
|
||||
check_to_execute = getattr(lib, check_name)
|
||||
check = check_to_execute()
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider"
|
||||
)
|
||||
continue
|
||||
if verbose:
|
||||
print(
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}"
|
||||
)
|
||||
check_findings = execute(
|
||||
check,
|
||||
service,
|
||||
check_name,
|
||||
global_provider,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
output_options,
|
||||
)
|
||||
report(check_findings, global_provider, output_options)
|
||||
all_findings.extend(check_findings)
|
||||
|
||||
# Update Audit Status
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
global_provider.audit_metadata = update_audit_metadata(
|
||||
global_provider.audit_metadata, services_executed, checks_executed
|
||||
)
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
@@ -535,39 +647,15 @@ def execute_checks(
|
||||
f"-> Scanning {orange_color}{service}{Style.RESET_ALL} service"
|
||||
)
|
||||
try:
|
||||
try:
|
||||
# Import check module
|
||||
check_module_path = f"prowler.providers.{global_provider.type}.services.{service}.{check_name}.{check_name}"
|
||||
lib = import_check(check_module_path)
|
||||
# Recover functions from check
|
||||
check_to_execute = getattr(lib, check_name)
|
||||
check = check_to_execute()
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider"
|
||||
)
|
||||
continue
|
||||
if verbose:
|
||||
print(
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}"
|
||||
)
|
||||
check_findings = execute(
|
||||
check,
|
||||
service,
|
||||
check_name,
|
||||
global_provider,
|
||||
custom_checks_metadata,
|
||||
output_options,
|
||||
)
|
||||
|
||||
report(check_findings, global_provider, output_options)
|
||||
|
||||
all_findings.extend(check_findings)
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
global_provider.audit_metadata = update_audit_metadata(
|
||||
global_provider.audit_metadata,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
all_findings.extend(check_findings)
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
@@ -582,79 +670,60 @@ def execute_checks(
|
||||
)
|
||||
bar()
|
||||
bar.title = f"-> {Fore.GREEN}Scan completed!{Style.RESET_ALL}"
|
||||
|
||||
# Custom report interface
|
||||
if os.environ.get("PROWLER_REPORT_LIB_PATH"):
|
||||
try:
|
||||
logger.info("Using custom report interface ...")
|
||||
lib = os.environ["PROWLER_REPORT_LIB_PATH"]
|
||||
outputs_module = importlib.import_module(lib)
|
||||
custom_report_interface = getattr(outputs_module, "report")
|
||||
|
||||
# TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider
|
||||
custom_report_interface(check_findings, output_options, global_provider)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
|
||||
return all_findings
|
||||
|
||||
|
||||
def execute(
|
||||
check: Check,
|
||||
service: str,
|
||||
check_name: str,
|
||||
global_provider: Any,
|
||||
services_executed: set,
|
||||
checks_executed: set,
|
||||
custom_checks_metadata: Any,
|
||||
output_options: Any = None,
|
||||
):
|
||||
"""
|
||||
Execute the check and report the findings
|
||||
|
||||
Args:
|
||||
service (str): service name
|
||||
check_name (str): check name
|
||||
global_provider (Any): provider object
|
||||
custom_checks_metadata (Any): custom checks metadata
|
||||
output_options (Any): output options, depending on the provider
|
||||
|
||||
Returns:
|
||||
list: list of findings
|
||||
"""
|
||||
try:
|
||||
# Import check module
|
||||
check_module_path = f"prowler.providers.{global_provider.type}.services.{service}.{check_name}.{check_name}"
|
||||
lib = import_check(check_module_path)
|
||||
# Recover functions from check
|
||||
check_to_execute = getattr(lib, check_name)
|
||||
check_class = check_to_execute()
|
||||
|
||||
# Update check metadata to reflect that in the outputs
|
||||
if custom_checks_metadata and custom_checks_metadata["Checks"].get(
|
||||
check.CheckID
|
||||
check_class.CheckID
|
||||
):
|
||||
check = update_check_metadata(
|
||||
check, custom_checks_metadata["Checks"][check.CheckID]
|
||||
check_class = update_check_metadata(
|
||||
check_class, custom_checks_metadata["Checks"][check_class.CheckID]
|
||||
)
|
||||
|
||||
only_logs = False
|
||||
if hasattr(output_options, "only_logs"):
|
||||
only_logs = output_options.only_logs
|
||||
|
||||
# Execute the check
|
||||
check_findings = []
|
||||
logger.debug(f"Executing check: {check.CheckID}")
|
||||
try:
|
||||
check_findings = check.execute()
|
||||
except Exception as error:
|
||||
if not only_logs:
|
||||
print(
|
||||
f"Something went wrong in {check.CheckID}, please use --log-level ERROR"
|
||||
)
|
||||
logger.error(
|
||||
f"{check.CheckID} -- {error.__class__.__name__}[{traceback.extract_tb(error.__traceback__)[-1].lineno}]: {error}"
|
||||
)
|
||||
# Run check
|
||||
verbose = (
|
||||
global_provider.output_options.verbose
|
||||
or global_provider.output_options.fixer
|
||||
)
|
||||
check_findings = run_check(
|
||||
check_class, verbose, global_provider.output_options.only_logs
|
||||
)
|
||||
|
||||
# Exclude findings per status
|
||||
if hasattr(output_options, "status") and output_options.status:
|
||||
if global_provider.output_options.status:
|
||||
check_findings = [
|
||||
finding
|
||||
for finding in check_findings
|
||||
if finding.status in output_options.status
|
||||
if finding.status in global_provider.output_options.status
|
||||
]
|
||||
|
||||
# Before returning the findings, we need to apply the mute list logic
|
||||
# Update Audit Status
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
global_provider.audit_metadata = update_audit_metadata(
|
||||
global_provider.audit_metadata, services_executed, checks_executed
|
||||
)
|
||||
|
||||
# Mutelist findings
|
||||
if hasattr(global_provider, "mutelist") and global_provider.mutelist.mutelist:
|
||||
# TODO: make this prettier
|
||||
is_finding_muted_args = {}
|
||||
if global_provider.type == "aws":
|
||||
is_finding_muted_args["aws_account_id"] = (
|
||||
@@ -669,9 +738,27 @@ def execute(
|
||||
**is_finding_muted_args
|
||||
)
|
||||
|
||||
# Refactor(Outputs)
|
||||
# Report the check's findings
|
||||
report(check_findings, global_provider)
|
||||
|
||||
# Refactor(Outputs)
|
||||
if os.environ.get("PROWLER_REPORT_LIB_PATH"):
|
||||
try:
|
||||
logger.info("Using custom report interface ...")
|
||||
lib = os.environ["PROWLER_REPORT_LIB_PATH"]
|
||||
outputs_module = importlib.import_module(lib)
|
||||
custom_report_interface = getattr(outputs_module, "report")
|
||||
|
||||
# TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider
|
||||
custom_report_interface(
|
||||
check_findings, global_provider.output_options, global_provider
|
||||
)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check.CheckID}' was not found for the {global_provider.type.upper()} provider"
|
||||
f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider"
|
||||
)
|
||||
check_findings = []
|
||||
except Exception as error:
|
||||
@@ -701,3 +788,34 @@ def update_audit_metadata(
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
def recover_checks_from_service(service_list: list, provider: str) -> set:
|
||||
"""
|
||||
Recover all checks from the selected provider and service
|
||||
|
||||
Returns a set of checks from the given services
|
||||
"""
|
||||
try:
|
||||
checks = set()
|
||||
service_list = [
|
||||
"awslambda" if service == "lambda" else service for service in service_list
|
||||
]
|
||||
for service in service_list:
|
||||
service_checks = recover_checks_from_provider(provider, service)
|
||||
if not service_checks:
|
||||
logger.error(f"Service '{service}' does not have checks.")
|
||||
|
||||
else:
|
||||
for check in service_checks:
|
||||
# Recover check name and module name from import path
|
||||
# Format: "providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
check_name = check[0].split(".")[-1]
|
||||
# If the service is present in the group list passed as parameters
|
||||
# if service_name in group_list: checks_from_arn.add(check_name)
|
||||
checks.add(check_name)
|
||||
return checks
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
@@ -4,8 +4,6 @@ from prowler.config.config import valid_severities
|
||||
from prowler.lib.check.check import (
|
||||
parse_checks_from_compliance_framework,
|
||||
parse_checks_from_file,
|
||||
)
|
||||
from prowler.lib.check.utils import (
|
||||
recover_checks_from_provider,
|
||||
recover_checks_from_service,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import sys
|
||||
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ def update_checks_metadata_with_compliance(
|
||||
if check in requirement.Checks:
|
||||
# Include the requirement into the check's framework requirements
|
||||
compliance_requirements.append(requirement)
|
||||
# Create the Compliance
|
||||
compliance = Compliance(
|
||||
# Create the Compliance_Model
|
||||
compliance = ComplianceBaseModel(
|
||||
Framework=framework.Framework,
|
||||
Provider=framework.Provider,
|
||||
Version=framework.Version,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import BaseModel, ValidationError, root_validator
|
||||
|
||||
from prowler.lib.check.utils import list_compliance_modules
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
@@ -169,19 +167,6 @@ class Mitre_Requirement(BaseModel):
|
||||
Checks: list[str]
|
||||
|
||||
|
||||
# KISA-ISMS-P Requirement Attribute
|
||||
class KISA_ISMSP_Requirement_Attribute(BaseModel):
|
||||
"""KISA ISMS-P Requirement Attribute"""
|
||||
|
||||
Domain: str
|
||||
Subdomain: str
|
||||
Section: str
|
||||
AuditChecklist: Optional[list[str]]
|
||||
RelatedRegulations: Optional[list[str]]
|
||||
AuditEvidence: Optional[list[str]]
|
||||
NonComplianceCases: Optional[list[str]]
|
||||
|
||||
|
||||
# Base Compliance Model
|
||||
# TODO: move this to compliance folder
|
||||
class Compliance_Requirement(BaseModel):
|
||||
@@ -196,7 +181,6 @@ class Compliance_Requirement(BaseModel):
|
||||
ENS_Requirement_Attribute,
|
||||
ISO27001_2013_Requirement_Attribute,
|
||||
AWS_Well_Architected_Requirement_Attribute,
|
||||
KISA_ISMSP_Requirement_Attribute,
|
||||
# Generic_Compliance_Requirement_Attribute must be the last one since it is the fallback for generic compliance framework
|
||||
Generic_Compliance_Requirement_Attribute,
|
||||
]
|
||||
@@ -204,8 +188,8 @@ class Compliance_Requirement(BaseModel):
|
||||
Checks: list[str]
|
||||
|
||||
|
||||
class Compliance(BaseModel):
|
||||
"""Compliance holds the base model for every compliance framework"""
|
||||
class ComplianceBaseModel(BaseModel):
|
||||
"""ComplianceBaseModel holds the base model for every compliance framework"""
|
||||
|
||||
Framework: str
|
||||
Provider: str
|
||||
@@ -229,137 +213,16 @@ class Compliance(BaseModel):
|
||||
raise ValueError("Framework or Provider must not be empty")
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def list(bulk_compliance_frameworks: dict, provider: str = None) -> list[str]:
|
||||
"""
|
||||
Returns a list of compliance frameworks from bulk compliance frameworks
|
||||
|
||||
Args:
|
||||
bulk_compliance_frameworks (dict): The bulk compliance frameworks
|
||||
provider (str): The provider name
|
||||
|
||||
Returns:
|
||||
list: The list of compliance frameworks
|
||||
"""
|
||||
if provider:
|
||||
compliance_frameworks = [
|
||||
compliance_framework
|
||||
for compliance_framework in bulk_compliance_frameworks.keys()
|
||||
if provider in compliance_framework
|
||||
]
|
||||
else:
|
||||
compliance_frameworks = [
|
||||
compliance_framework
|
||||
for compliance_framework in bulk_compliance_frameworks.keys()
|
||||
]
|
||||
|
||||
return compliance_frameworks
|
||||
|
||||
@staticmethod
|
||||
def get(
|
||||
bulk_compliance_frameworks: dict, compliance_framework_name: str
|
||||
) -> "Compliance":
|
||||
"""
|
||||
Returns a compliance framework from bulk compliance frameworks
|
||||
|
||||
Args:
|
||||
bulk_compliance_frameworks (dict): The bulk compliance frameworks
|
||||
compliance_framework_name (str): The compliance framework name
|
||||
|
||||
Returns:
|
||||
Compliance: The compliance framework
|
||||
"""
|
||||
return bulk_compliance_frameworks.get(compliance_framework_name, None)
|
||||
|
||||
@staticmethod
|
||||
def list_requirements(
|
||||
bulk_compliance_frameworks: dict, compliance_framework: str = None
|
||||
) -> list:
|
||||
"""
|
||||
Returns a list of compliance requirements from a compliance framework
|
||||
|
||||
Args:
|
||||
bulk_compliance_frameworks (dict): The bulk compliance frameworks
|
||||
compliance_framework (str): The compliance framework name
|
||||
|
||||
Returns:
|
||||
list: The list of compliance requirements for the provided compliance framework
|
||||
"""
|
||||
compliance_requirements = []
|
||||
|
||||
if bulk_compliance_frameworks and compliance_framework:
|
||||
compliance_requirements = [
|
||||
compliance_requirement.Id
|
||||
for compliance_requirement in bulk_compliance_frameworks.get(
|
||||
compliance_framework
|
||||
).Requirements
|
||||
]
|
||||
|
||||
return compliance_requirements
|
||||
|
||||
@staticmethod
|
||||
def get_requirement(
|
||||
bulk_compliance_frameworks: dict, compliance_framework: str, requirement_id: str
|
||||
) -> Union[Mitre_Requirement, Compliance_Requirement]:
|
||||
"""
|
||||
Returns a compliance requirement from a compliance framework
|
||||
|
||||
Args:
|
||||
bulk_compliance_frameworks (dict): The bulk compliance frameworks
|
||||
compliance_framework (str): The compliance framework name
|
||||
requirement_id (str): The compliance requirement ID
|
||||
|
||||
Returns:
|
||||
Mitre_Requirement | Compliance_Requirement: The compliance requirement
|
||||
"""
|
||||
requirement = None
|
||||
for compliance_requirement in bulk_compliance_frameworks.get(
|
||||
compliance_framework
|
||||
).Requirements:
|
||||
if compliance_requirement.Id == requirement_id:
|
||||
requirement = compliance_requirement
|
||||
break
|
||||
|
||||
return requirement
|
||||
|
||||
@staticmethod
|
||||
def get_bulk(provider: str) -> dict:
|
||||
"""Bulk load all compliance frameworks specification into a dict"""
|
||||
try:
|
||||
bulk_compliance_frameworks = {}
|
||||
available_compliance_framework_modules = list_compliance_modules()
|
||||
for compliance_framework in available_compliance_framework_modules:
|
||||
if provider in compliance_framework.name:
|
||||
compliance_specification_dir_path = (
|
||||
f"{compliance_framework.module_finder.path}/{provider}"
|
||||
)
|
||||
# for compliance_framework in available_compliance_framework_modules:
|
||||
for filename in os.listdir(compliance_specification_dir_path):
|
||||
file_path = os.path.join(
|
||||
compliance_specification_dir_path, filename
|
||||
)
|
||||
# Check if it is a file and ti size is greater than 0
|
||||
if os.path.isfile(file_path) and os.stat(file_path).st_size > 0:
|
||||
# Open Compliance file in JSON
|
||||
# cis_v1.4_aws.json --> cis_v1.4_aws
|
||||
compliance_framework_name = filename.split(".json")[0]
|
||||
# Store the compliance info
|
||||
bulk_compliance_frameworks[compliance_framework_name] = (
|
||||
load_compliance_framework(file_path)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}] -- {e}")
|
||||
|
||||
return bulk_compliance_frameworks
|
||||
|
||||
|
||||
# Testing Pending
|
||||
def load_compliance_framework(
|
||||
compliance_specification_file: str,
|
||||
) -> Compliance:
|
||||
) -> ComplianceBaseModel:
|
||||
"""load_compliance_framework loads and parse a Compliance Framework Specification"""
|
||||
try:
|
||||
compliance_framework = Compliance.parse_file(compliance_specification_file)
|
||||
compliance_framework = ComplianceBaseModel.parse_file(
|
||||
compliance_specification_file
|
||||
)
|
||||
except ValidationError as error:
|
||||
logger.critical(
|
||||
f"Compliance Framework Specification from {compliance_specification_file} is not valid: {error}"
|
||||
|
||||
@@ -7,20 +7,11 @@ from dataclasses import dataclass
|
||||
from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
from prowler.config.config import valid_severities
|
||||
from prowler.lib.check.utils import recover_checks_from_provider
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
class Code(BaseModel):
|
||||
"""
|
||||
Represents the remediation code using IaC like CloudFormation, Terraform or the native CLI.
|
||||
|
||||
Attributes:
|
||||
NativeIaC (str): The NativeIaC code.
|
||||
Terraform (str): The Terraform code.
|
||||
CLI (str): The CLI code.
|
||||
Other (str): Other code.
|
||||
"""
|
||||
"""Check's remediation information using IaC like CloudFormation, Terraform or the native CLI"""
|
||||
|
||||
NativeIaC: str
|
||||
Terraform: str
|
||||
@@ -29,61 +20,21 @@ class Code(BaseModel):
|
||||
|
||||
|
||||
class Recommendation(BaseModel):
|
||||
"""
|
||||
Represents a recommendation.
|
||||
|
||||
Attributes:
|
||||
Text (str): The text of the recommendation.
|
||||
Url (str): The URL associated with the recommendation.
|
||||
"""
|
||||
"""Check's recommendation information"""
|
||||
|
||||
Text: str
|
||||
Url: str
|
||||
|
||||
|
||||
class Remediation(BaseModel):
|
||||
"""
|
||||
Represents a remediation action for a specific .
|
||||
|
||||
Attributes:
|
||||
Code (Code): The code associated with the remediation action.
|
||||
Recommendation (Recommendation): The recommendation for the remediation action.
|
||||
"""
|
||||
"""Check's remediation: Code and Recommendation"""
|
||||
|
||||
Code: Code
|
||||
Recommendation: Recommendation
|
||||
|
||||
|
||||
class CheckMetadata(BaseModel):
|
||||
"""
|
||||
Model representing the metadata of a check.
|
||||
|
||||
Attributes:
|
||||
Provider (str): The provider of the check.
|
||||
CheckID (str): The ID of the check.
|
||||
CheckTitle (str): The title of the check.
|
||||
CheckType (list[str]): The type of the check.
|
||||
CheckAliases (list[str], optional): The aliases of the check. Defaults to an empty list.
|
||||
ServiceName (str): The name of the service.
|
||||
SubServiceName (str): The name of the sub-service.
|
||||
ResourceIdTemplate (str): The template for the resource ID.
|
||||
Severity (str): The severity of the check.
|
||||
ResourceType (str): The type of the resource.
|
||||
Description (str): The description of the check.
|
||||
Risk (str): The risk associated with the check.
|
||||
RelatedUrl (str): The URL related to the check.
|
||||
Remediation (Remediation): The remediation steps for the check.
|
||||
Categories (list[str]): The categories of the check.
|
||||
DependsOn (list[str]): The dependencies of the check.
|
||||
RelatedTo (list[str]): The related checks.
|
||||
Notes (str): Additional notes for the check.
|
||||
Compliance (list, optional): The compliance information for the check. Defaults to None.
|
||||
|
||||
Validators:
|
||||
valid_category(value): Validator function to validate the categories of the check.
|
||||
severity_to_lower(severity): Validator function to convert the severity to lowercase.
|
||||
valid_severity(severity): Validator function to validate the severity of the check.
|
||||
"""
|
||||
class Check_Metadata_Model(BaseModel):
|
||||
"""Check Metadata Model"""
|
||||
|
||||
Provider: str
|
||||
CheckID: str
|
||||
@@ -130,36 +81,8 @@ class CheckMetadata(BaseModel):
|
||||
)
|
||||
return severity
|
||||
|
||||
@staticmethod
|
||||
def get_bulk(provider: str) -> dict[str, "CheckMetadata"]:
|
||||
"""
|
||||
Load the metadata of all checks for a given provider reading the check's metadata files.
|
||||
Args:
|
||||
provider (str): The name of the provider.
|
||||
Returns:
|
||||
dict[str, CheckMetadata]: A dictionary containing the metadata of all checks, with the CheckID as the key.
|
||||
"""
|
||||
|
||||
bulk_check_metadata = {}
|
||||
checks = recover_checks_from_provider(provider)
|
||||
# Build list of check's metadata files
|
||||
for check_info in checks:
|
||||
# Build check path name
|
||||
check_name = check_info[0]
|
||||
check_path = check_info[1]
|
||||
# Ignore fixer files
|
||||
if check_name.endswith("_fixer"):
|
||||
continue
|
||||
# Append metadata file extension
|
||||
metadata_file = f"{check_path}/{check_name}.metadata.json"
|
||||
# Load metadata
|
||||
check_metadata = load_check_metadata(metadata_file)
|
||||
bulk_check_metadata[check_metadata.CheckID] = check_metadata
|
||||
|
||||
return bulk_check_metadata
|
||||
|
||||
|
||||
class Check(ABC, CheckMetadata):
|
||||
class Check(ABC, Check_Metadata_Model):
|
||||
"""Prowler Check"""
|
||||
|
||||
def __init__(self, **data):
|
||||
@@ -170,7 +93,7 @@ class Check(ABC, CheckMetadata):
|
||||
+ ".metadata.json"
|
||||
)
|
||||
# Store it to validate them with Pydantic
|
||||
data = CheckMetadata.parse_file(metadata_file).dict()
|
||||
data = Check_Metadata_Model.parse_file(metadata_file).dict()
|
||||
# Calls parents init function
|
||||
super().__init__(**data)
|
||||
# TODO: verify that the CheckID is the same as the filename and classname
|
||||
@@ -191,14 +114,14 @@ class Check_Report:
|
||||
|
||||
status: str
|
||||
status_extended: str
|
||||
check_metadata: CheckMetadata
|
||||
check_metadata: Check_Metadata_Model
|
||||
resource_details: str
|
||||
resource_tags: list
|
||||
muted: bool
|
||||
|
||||
def __init__(self, metadata):
|
||||
self.status = ""
|
||||
self.check_metadata = CheckMetadata.parse_raw(metadata)
|
||||
self.check_metadata = Check_Metadata_Model.parse_raw(metadata)
|
||||
self.status_extended = ""
|
||||
self.resource_details = ""
|
||||
self.resource_tags = []
|
||||
@@ -271,22 +194,12 @@ class Check_Report_Kubernetes(Check_Report):
|
||||
|
||||
|
||||
# Testing Pending
|
||||
def load_check_metadata(metadata_file: str) -> CheckMetadata:
|
||||
"""
|
||||
Load check metadata from a file.
|
||||
Args:
|
||||
metadata_file (str): The path to the metadata file.
|
||||
Returns:
|
||||
CheckMetadata: The loaded check metadata.
|
||||
Raises:
|
||||
ValidationError: If the metadata file is not valid.
|
||||
"""
|
||||
|
||||
def load_check_metadata(metadata_file: str) -> Check_Metadata_Model:
|
||||
"""load_check_metadata loads and parse a Check's metadata file"""
|
||||
try:
|
||||
check_metadata = CheckMetadata.parse_file(metadata_file)
|
||||
check_metadata = Check_Metadata_Model.parse_file(metadata_file)
|
||||
except ValidationError as error:
|
||||
logger.critical(f"Metadata from {metadata_file} is not valid: {error}")
|
||||
# TODO: remove this exit and raise an exception
|
||||
sys.exit(1)
|
||||
else:
|
||||
return check_metadata
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import importlib
|
||||
import sys
|
||||
from pkgutil import walk_packages
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
def recover_checks_from_provider(
|
||||
provider: str, service: str = None, include_fixers: bool = False
|
||||
) -> list[tuple]:
|
||||
"""
|
||||
Recover all checks from the selected provider and service
|
||||
|
||||
Returns a list of tuples with the following format (check_name, check_path)
|
||||
"""
|
||||
try:
|
||||
checks = []
|
||||
modules = list_modules(provider, service)
|
||||
for module_name in modules:
|
||||
# Format: "prowler.providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
check_module_name = module_name.name
|
||||
# We need to exclude common shared libraries in services
|
||||
if (
|
||||
check_module_name.count(".") == 6
|
||||
and "lib" not in check_module_name
|
||||
and (not check_module_name.endswith("_fixer") or include_fixers)
|
||||
):
|
||||
check_path = module_name.module_finder.path
|
||||
# Check name is the last part of the check_module_name
|
||||
check_name = check_module_name.split(".")[-1]
|
||||
check_info = (check_name, check_path)
|
||||
checks.append(check_info)
|
||||
except ModuleNotFoundError:
|
||||
logger.critical(f"Service {service} was not found for the {provider} provider.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.critical(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
return checks
|
||||
|
||||
|
||||
# List all available modules in the selected provider and service
|
||||
def list_modules(provider: str, service: str):
|
||||
# This module path requires the full path including "prowler."
|
||||
module_path = f"prowler.providers.{provider}.services"
|
||||
if service:
|
||||
module_path += f".{service}"
|
||||
return walk_packages(
|
||||
importlib.import_module(module_path).__path__,
|
||||
importlib.import_module(module_path).__name__ + ".",
|
||||
)
|
||||
|
||||
|
||||
def recover_checks_from_service(service_list: list, provider: str) -> set:
|
||||
"""
|
||||
Recover all checks from the selected provider and service
|
||||
|
||||
Returns a set of checks from the given services
|
||||
"""
|
||||
try:
|
||||
checks = set()
|
||||
service_list = [
|
||||
"awslambda" if service == "lambda" else service for service in service_list
|
||||
]
|
||||
for service in service_list:
|
||||
service_checks = recover_checks_from_provider(provider, service)
|
||||
if not service_checks:
|
||||
logger.error(f"Service '{service}' does not have checks.")
|
||||
|
||||
else:
|
||||
for check in service_checks:
|
||||
# Recover check name and module name from import path
|
||||
# Format: "providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
check_name = check[0].split(".")[-1]
|
||||
# If the service is present in the group list passed as parameters
|
||||
# if service_name in group_list: checks_from_arn.add(check_name)
|
||||
checks.add(check_name)
|
||||
return checks
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
def list_compliance_modules():
|
||||
"""
|
||||
list_compliance_modules returns the available compliance frameworks and returns their path
|
||||
"""
|
||||
# This module path requires the full path including "prowler."
|
||||
module_path = "prowler.compliance"
|
||||
return walk_packages(
|
||||
importlib.import_module(module_path).__path__,
|
||||
importlib.import_module(module_path).__name__ + ".",
|
||||
)
|
||||
@@ -314,9 +314,6 @@ class Mutelist(ABC):
|
||||
Args:
|
||||
matched_items (list): List of items to be matched.
|
||||
finding_items (str): String to search for matched items.
|
||||
tag (bool): If True the search will have a different logic due to the tags being ANDed or ORed:
|
||||
- Check of AND logic -> True if all the tags are present in the finding.
|
||||
- Check of OR logic -> True if any of the tags is present in the finding.
|
||||
|
||||
Returns:
|
||||
bool: True if any of the matched_items are present in finding_items, otherwise False.
|
||||
|
||||
@@ -89,11 +89,7 @@ class ASFF(Output):
|
||||
CreatedAt=timestamp,
|
||||
Severity=Severity(Label=finding.severity.value),
|
||||
Title=finding.check_title,
|
||||
Description=(
|
||||
(finding.status_extended[:1000] + "...")
|
||||
if len(finding.status_extended) > 1000
|
||||
else finding.status_extended
|
||||
),
|
||||
Description=finding.description,
|
||||
Resources=[
|
||||
Resource(
|
||||
Id=finding.resource_uid,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.aws_well_architected.models import (
|
||||
AWSWellArchitectedModel,
|
||||
)
|
||||
@@ -21,7 +21,7 @@ class AWSWellArchitected(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -29,7 +29,7 @@ class AWSWellArchitected(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.cis.models import AWSCISModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -19,7 +19,7 @@ class AWSCIS(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class AWSCIS(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.cis.models import AzureCISModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -19,7 +19,7 @@ class AzureCIS(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class AzureCIS(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.cis.models import GCPCISModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -19,7 +19,7 @@ class GCPCIS(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class GCPCIS(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.cis.models import KubernetesCISModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -21,7 +21,7 @@ class KubernetesCIS(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -29,7 +29,7 @@ class KubernetesCIS(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -7,7 +7,6 @@ from prowler.lib.outputs.compliance.ens.ens import get_ens_table
|
||||
from prowler.lib.outputs.compliance.generic.generic_table import (
|
||||
get_generic_compliance_table,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp import get_kisa_ismsp_table
|
||||
from prowler.lib.outputs.compliance.mitre_attack.mitre_attack import (
|
||||
get_mitre_attack_table,
|
||||
)
|
||||
@@ -63,15 +62,6 @@ def display_compliance_table(
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
elif "kisa_isms_" in compliance_framework:
|
||||
get_kisa_ismsp_table(
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance_framework,
|
||||
output_filename,
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
else:
|
||||
get_generic_compliance_table(
|
||||
findings,
|
||||
|
||||
@@ -2,7 +2,7 @@ from csv import DictWriter
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.lib.outputs.output import Output
|
||||
@@ -28,7 +28,7 @@ class ComplianceOutput(Output):
|
||||
def __init__(
|
||||
self,
|
||||
findings: List[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
create_file_descriptor: bool = False,
|
||||
file_path: str = None,
|
||||
file_extension: str = "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.ens.models import AWSENSModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -19,7 +19,7 @@ class AWSENS(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class AWSENS(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.generic.models import GenericComplianceModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -19,7 +19,7 @@ class GenericCompliance(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class GenericCompliance(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.iso27001.models import AWSISO27001Model
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -19,7 +19,7 @@ class AWSISO27001(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class AWSISO27001(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color
|
||||
|
||||
|
||||
def get_kisa_ismsp_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
sections = {}
|
||||
kisa_ismsp_compliance_table = {
|
||||
"Provider": [],
|
||||
"Section": [],
|
||||
"Status": [],
|
||||
"Muted": [],
|
||||
}
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if (
|
||||
compliance.Framework.startswith("KISA")
|
||||
and compliance.Version in compliance_framework
|
||||
):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
# Check if Section exists
|
||||
if section not in sections:
|
||||
sections[section] = {
|
||||
"Status": f"{Fore.GREEN}PASS{Style.RESET_ALL}",
|
||||
"Muted": 0,
|
||||
}
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
fail_count.append(index)
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Add results to table
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
kisa_ismsp_compliance_table["Provider"].append(compliance.Provider)
|
||||
kisa_ismsp_compliance_table["Section"].append(section)
|
||||
kisa_ismsp_compliance_table["Muted"].append(
|
||||
f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}"
|
||||
)
|
||||
if len(fail_count) + len(pass_count) + len(muted_count) > 1:
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
print(
|
||||
f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
|
||||
)
|
||||
print(
|
||||
tabulate(
|
||||
kisa_ismsp_compliance_table,
|
||||
headers="keys",
|
||||
tablefmt="rounded_grid",
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"{Style.BRIGHT}* Only sections containing results appear.{Style.RESET_ALL}"
|
||||
)
|
||||
print(f"\nDetailed results of {compliance_framework.upper()} are in:")
|
||||
print(
|
||||
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
|
||||
)
|
||||
@@ -1,93 +0,0 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.kisa_ismsp.models import AWSKISAISMSPModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class AWSKISAISMSP(ComplianceOutput):
|
||||
"""
|
||||
This class represents the AWS KISA-ISMS-P compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into AWS KISA-ISMS-P compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into AWS KISA-ISMS-P compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
# Get the compliance requirements for the finding
|
||||
finding_requirements = finding.compliance.get(compliance_name, [])
|
||||
for requirement in compliance.Requirements:
|
||||
if requirement.Id in finding_requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AWSKISAISMSPModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=finding.account_uid,
|
||||
Region=finding.region,
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Domain=attribute.Domain,
|
||||
Requirements_Attributes_Subdomain=attribute.Subdomain,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_AuditChecklist=attribute.AuditChecklist,
|
||||
Requirements_Attributes_RelatedRegulations=attribute.RelatedRegulations,
|
||||
Requirements_Attributes_AuditEvidence=attribute.AuditEvidence,
|
||||
Requirements_Attributes_NonComplianceCases=attribute.NonComplianceCases,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AWSKISAISMSPModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
AccountId="",
|
||||
Region="",
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Domain=attribute.Domain,
|
||||
Requirements_Attributes_Subdomain=attribute.Subdomain,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_AuditChecklist=attribute.AuditChecklist,
|
||||
Requirements_Attributes_RelatedRegulations=attribute.RelatedRegulations,
|
||||
Requirements_Attributes_AuditEvidence=attribute.AuditEvidence,
|
||||
Requirements_Attributes_NonComplianceCases=attribute.NonComplianceCases,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -1,31 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AWSKISAISMSPModel(BaseModel):
|
||||
"""
|
||||
The AWS KISA-ISMS-P Model outputs findings in a format compliant with the AWS KISA-ISMS-P standard
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
AccountId: str
|
||||
Region: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Name: str
|
||||
Requirements_Description: str
|
||||
Requirements_Attributes_Domain: str
|
||||
Requirements_Attributes_Subdomain: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_AuditChecklist: Optional[list[str]]
|
||||
Requirements_Attributes_RelatedRegulations: Optional[list[str]]
|
||||
Requirements_Attributes_AuditEvidence: Optional[list[str]]
|
||||
Requirements_Attributes_NonComplianceCases: Optional[list[str]]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
ResourceName: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.mitre_attack.models import AWSMitreAttackModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -20,7 +20,7 @@ class AWSMitreAttack(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -28,7 +28,7 @@ class AWSMitreAttack(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.mitre_attack.models import AzureMitreAttackModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -20,7 +20,7 @@ class AzureMitreAttack(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -28,7 +28,7 @@ class AzureMitreAttack(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import ComplianceBaseModel
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.mitre_attack.models import GCPMitreAttackModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
@@ -20,7 +20,7 @@ class GCPMitreAttack(ComplianceOutput):
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance: ComplianceBaseModel,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -28,7 +28,7 @@ class GCPMitreAttack(ComplianceOutput):
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance (ComplianceBaseModel): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -88,37 +88,29 @@ class Finding(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def generate_output(
|
||||
cls, provider: Provider, check_output: Check_Report, output_options
|
||||
cls, provider: Provider, check_output: Check_Report
|
||||
) -> "Finding":
|
||||
"""Generates the output for a finding based on the provider and output options
|
||||
|
||||
Args:
|
||||
provider (Provider): the provider object
|
||||
check_output (Check_Report): the check output object
|
||||
output_options: the output options object, depending on the provider
|
||||
Returns:
|
||||
finding_output (Finding): the finding output object
|
||||
|
||||
"""
|
||||
output_options = provider.output_options
|
||||
# TODO: think about get_provider_data_mapping
|
||||
provider_data_mapping = get_provider_data_mapping(provider)
|
||||
|
||||
# TODO: move fill_common_finding_data
|
||||
unix_timestamp = False
|
||||
if hasattr(output_options, "unix_timestamp"):
|
||||
unix_timestamp = output_options.unix_timestamp
|
||||
|
||||
common_finding_data = fill_common_finding_data(check_output, unix_timestamp)
|
||||
common_finding_data = fill_common_finding_data(
|
||||
check_output, output_options.unix_timestamp
|
||||
)
|
||||
output_data = {}
|
||||
output_data.update(provider_data_mapping)
|
||||
output_data.update(common_finding_data)
|
||||
|
||||
bulk_checks_metadata = {}
|
||||
if hasattr(output_options, "bulk_checks_metadata"):
|
||||
bulk_checks_metadata = output_options.bulk_checks_metadata
|
||||
|
||||
output_data["compliance"] = get_check_compliance(
|
||||
check_output, provider.type, bulk_checks_metadata
|
||||
check_output, provider.type, output_options.bulk_checks_metadata
|
||||
)
|
||||
try:
|
||||
if provider.type == "aws":
|
||||
|
||||
@@ -173,15 +173,9 @@ class HTML(Output):
|
||||
<li class="list-group-item">
|
||||
<b>Passed:</b> {str(stats.get("total_pass", 0))}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Passed (Muted):</b> {str(stats.get("total_muted_pass", 0))}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Failed:</b> {str(stats.get("total_fail", 0))}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Failed (Muted):</b> {str(stats.get("total_muted_fail", 0))}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Total Resources:</b> {str(stats.get("resources_count", 0))}
|
||||
</li>
|
||||
|
||||
@@ -25,12 +25,10 @@ def stdout_report(finding, color, verbose, status, fix):
|
||||
)
|
||||
|
||||
|
||||
# TODO: Only pass check_findings, output_options and provider.type
|
||||
def report(check_findings, provider, output_options):
|
||||
# TODO: Only pass check_findings, provider.output_options and provider.type
|
||||
def report(check_findings, provider):
|
||||
try:
|
||||
verbose = False
|
||||
if hasattr(output_options, "verbose"):
|
||||
verbose = output_options.verbose
|
||||
output_options = provider.output_options
|
||||
if check_findings:
|
||||
# TO-DO Generic Function
|
||||
if provider.type == "aws":
|
||||
@@ -41,27 +39,21 @@ def report(check_findings, provider, output_options):
|
||||
|
||||
for finding in check_findings:
|
||||
# Print findings by stdout
|
||||
status = []
|
||||
if hasattr(output_options, "status"):
|
||||
status = output_options.status
|
||||
fixer = False
|
||||
if hasattr(output_options, "fixer"):
|
||||
fixer = output_options.fixer
|
||||
color = set_report_color(finding.status, finding.muted)
|
||||
stdout_report(
|
||||
finding,
|
||||
color,
|
||||
verbose,
|
||||
status,
|
||||
fixer,
|
||||
output_options.verbose,
|
||||
output_options.status,
|
||||
output_options.fixer,
|
||||
)
|
||||
|
||||
else: # No service resources in the whole account
|
||||
color = set_report_color("MANUAL")
|
||||
if verbose:
|
||||
if output_options.verbose:
|
||||
print(f"\t{color}INFO{Style.RESET_ALL} There are no resources")
|
||||
# Separator between findings and bar
|
||||
if verbose:
|
||||
if output_options.verbose:
|
||||
print()
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -90,82 +82,35 @@ def extract_findings_statistics(findings: list) -> dict:
|
||||
extract_findings_statistics takes a list of findings and returns the following dict with the aggregated statistics
|
||||
{
|
||||
"total_pass": 0,
|
||||
"total_muted_pass": 0,
|
||||
"total_fail": 0,
|
||||
"total_muted_fail": 0,
|
||||
"resources_count": 0,
|
||||
"findings_count": 0,
|
||||
"critical_failed_findings": [],
|
||||
"critical_passed_findings": []
|
||||
"all_fails_are_muted": False
|
||||
}
|
||||
"""
|
||||
logger.info("Extracting audit statistics...")
|
||||
stats = {}
|
||||
total_pass = 0
|
||||
total_fail = 0
|
||||
muted_pass = 0
|
||||
muted_fail = 0
|
||||
resources = set()
|
||||
findings_count = 0
|
||||
all_fails_are_muted = True
|
||||
critical_severity_pass = 0
|
||||
critical_severity_fail = 0
|
||||
high_severity_pass = 0
|
||||
high_severity_fail = 0
|
||||
medium_severity_pass = 0
|
||||
medium_severity_fail = 0
|
||||
low_severity_pass = 0
|
||||
low_severity_fail = 0
|
||||
|
||||
for finding in findings:
|
||||
# Save the resource_id
|
||||
resources.add(finding.resource_id)
|
||||
|
||||
if finding.status == "PASS":
|
||||
if finding.check_metadata.Severity == "critical":
|
||||
critical_severity_pass += 1
|
||||
if finding.check_metadata.Severity == "high":
|
||||
high_severity_pass += 1
|
||||
if finding.check_metadata.Severity == "medium":
|
||||
medium_severity_pass += 1
|
||||
if finding.check_metadata.Severity == "low":
|
||||
low_severity_pass += 1
|
||||
total_pass += 1
|
||||
findings_count += 1
|
||||
if finding.muted is True:
|
||||
muted_pass += 1
|
||||
|
||||
if finding.status == "FAIL":
|
||||
if finding.check_metadata.Severity == "critical":
|
||||
critical_severity_fail += 1
|
||||
if finding.check_metadata.Severity == "high":
|
||||
high_severity_fail += 1
|
||||
if finding.check_metadata.Severity == "medium":
|
||||
medium_severity_fail += 1
|
||||
if finding.check_metadata.Severity == "low":
|
||||
low_severity_fail += 1
|
||||
total_fail += 1
|
||||
findings_count += 1
|
||||
if finding.muted is True:
|
||||
muted_fail += 1
|
||||
if not finding.muted and all_fails_are_muted:
|
||||
all_fails_are_muted = False
|
||||
|
||||
stats["total_pass"] = total_pass
|
||||
stats["total_muted_pass"] = muted_pass
|
||||
stats["total_fail"] = total_fail
|
||||
stats["total_muted_fail"] = muted_fail
|
||||
stats["resources_count"] = len(resources)
|
||||
stats["findings_count"] = findings_count
|
||||
stats["total_critical_severity_fail"] = critical_severity_fail
|
||||
stats["total_critical_severity_pass"] = critical_severity_pass
|
||||
stats["total_high_severity_fail"] = high_severity_fail
|
||||
stats["total_high_severity_pass"] = high_severity_pass
|
||||
stats["total_medium_severity_fail"] = medium_severity_fail
|
||||
stats["total_medium_severity_pass"] = medium_severity_pass
|
||||
stats["total_low_severity_fail"] = medium_severity_fail
|
||||
stats["total_low_severity_pass"] = medium_severity_pass
|
||||
stats["all_fails_are_muted"] = all_fails_are_muted
|
||||
|
||||
return stats
|
||||
|
||||
@@ -121,19 +121,6 @@ class Slack:
|
||||
"text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": (
|
||||
"*Severities:*\n"
|
||||
f"• *Critical:* {stats['total_critical_severity_pass']} "
|
||||
f"• *High:* {stats['total_high_severity_pass']} "
|
||||
f"• *Medium:* {stats['total_medium_severity_pass']} "
|
||||
f"• *Low:* {stats['total_low_severity_pass']}"
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
@@ -141,19 +128,6 @@ class Slack:
|
||||
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": (
|
||||
"*Severities:*\n"
|
||||
f"• *Critical:* {stats['total_critical_severity_fail']} "
|
||||
f"• *High:* {stats['total_high_severity_fail']} "
|
||||
f"• *Medium:* {stats['total_medium_severity_fail']} "
|
||||
f"• *Low:* {stats['total_low_severity_fail']}"
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
|
||||
@@ -52,14 +52,6 @@ def unroll_tags(tags: list) -> dict:
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
|
||||
>>> tags = [{"key": "name"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': ''}
|
||||
|
||||
>>> tags = [{"Key": "name"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': ''}
|
||||
|
||||
>>> tags = [{"name": "John", "age": "30"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
@@ -82,9 +74,9 @@ def unroll_tags(tags: list) -> dict:
|
||||
if isinstance(tags[0], str) and len(tags) > 0:
|
||||
return {tag: "" for tag in tags}
|
||||
if "key" in tags[0]:
|
||||
return {item["key"]: item.get("value", "") for item in tags}
|
||||
return {item["key"]: item["value"] for item in tags}
|
||||
elif "Key" in tags[0]:
|
||||
return {item["Key"]: item.get("Value", "") for item in tags}
|
||||
return {item["Key"]: item["Value"] for item in tags}
|
||||
else:
|
||||
return {key: value for d in tags for key, value in d.items()}
|
||||
return {}
|
||||
|
||||
@@ -1,215 +1,60 @@
|
||||
from typing import Generator
|
||||
from typing import Any
|
||||
|
||||
from prowler.lib.check.check import execute, import_check, update_audit_metadata
|
||||
from prowler.lib.check.check import execute
|
||||
from prowler.lib.check.models import Check_Report
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
class Scan:
|
||||
_provider: Provider
|
||||
# Refactor(Core): This should replace the Audit_Metadata
|
||||
_number_of_checks_to_execute: int = 0
|
||||
_number_of_checks_completed: int = 0
|
||||
# TODO the str should be a set of Check objects
|
||||
_checks_to_execute: list[str]
|
||||
_service_checks_to_execute: dict[str, set[str]]
|
||||
_service_checks_completed: dict[str, set[str]]
|
||||
_progress: float = 0.0
|
||||
_findings: list = []
|
||||
def scan(
|
||||
checks_to_execute: list,
|
||||
global_provider: Any,
|
||||
custom_checks_metadata: Any,
|
||||
) -> list[Check_Report]:
|
||||
try:
|
||||
# List to store all the check's findings
|
||||
all_findings = []
|
||||
# Services and checks executed for the Audit Status
|
||||
services_executed = set()
|
||||
checks_executed = set()
|
||||
|
||||
def __init__(self, provider: Provider, checks_to_execute: list[str]):
|
||||
"""
|
||||
Scan is the class that executes the checks and yields the progress and the findings.
|
||||
|
||||
Params:
|
||||
provider: Provider -> The provider to scan
|
||||
checks_to_execute: list[str] -> The checks to execute
|
||||
"""
|
||||
self._provider = provider
|
||||
# Remove duplicated checks and sort them
|
||||
self._checks_to_execute = sorted(list(set(checks_to_execute)))
|
||||
|
||||
self._number_of_checks_to_execute = len(checks_to_execute)
|
||||
|
||||
service_checks_to_execute = get_service_checks_to_execute(checks_to_execute)
|
||||
service_checks_completed = dict()
|
||||
|
||||
self._service_checks_to_execute = service_checks_to_execute
|
||||
self._service_checks_completed = service_checks_completed
|
||||
|
||||
@property
|
||||
def checks_to_execute(self) -> set[str]:
|
||||
return self._checks_to_execute
|
||||
|
||||
@property
|
||||
def service_checks_to_execute(self) -> dict[str, set[str]]:
|
||||
return self._service_checks_to_execute
|
||||
|
||||
@property
|
||||
def service_checks_completed(self) -> dict[str, set[str]]:
|
||||
return self._service_checks_completed
|
||||
|
||||
@property
|
||||
def provider(self) -> Provider:
|
||||
return self._provider
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return (
|
||||
self._number_of_checks_completed / self._number_of_checks_to_execute * 100
|
||||
# Initialize the Audit Metadata
|
||||
# TODO: this should be done in the provider class
|
||||
# Refactor(Core): Audit manager?
|
||||
global_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=checks_to_execute,
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
@property
|
||||
def findings(self) -> list:
|
||||
return self._findings
|
||||
for check_name in checks_to_execute:
|
||||
try:
|
||||
# Recover service from check name
|
||||
service = check_name.split("_")[0]
|
||||
|
||||
def scan(
|
||||
self,
|
||||
custom_checks_metadata: dict = {},
|
||||
) -> Generator[tuple[float, list[Finding]], None, None]:
|
||||
"""
|
||||
Executes the scan by iterating over the checks to execute and executing each check.
|
||||
Yields the progress and findings for each check.
|
||||
check_findings = execute(
|
||||
service,
|
||||
check_name,
|
||||
global_provider,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
all_findings.extend(check_findings)
|
||||
|
||||
Args:
|
||||
custom_checks_metadata (dict): Custom metadata for the checks (default: {}).
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
Yields:
|
||||
Tuple[float, list[Finding]]: A tuple containing the progress and findings for each check.
|
||||
|
||||
Raises:
|
||||
ModuleNotFoundError: If the check does not exist in the provider or is from another provider.
|
||||
Exception: If any other error occurs during the execution of a check.
|
||||
"""
|
||||
try:
|
||||
checks_to_execute = self.checks_to_execute
|
||||
# Initialize the Audit Metadata
|
||||
# TODO: this should be done in the provider class
|
||||
# Refactor(Core): Audit manager?
|
||||
self._provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=checks_to_execute,
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
for check_name in checks_to_execute:
|
||||
try:
|
||||
# Recover service from check name
|
||||
service = get_service_name_from_check_name(check_name)
|
||||
try:
|
||||
# Import check module
|
||||
check_module_path = f"prowler.providers.{self._provider.type}.services.{service}.{check_name}.{check_name}"
|
||||
lib = import_check(check_module_path)
|
||||
# Recover functions from check
|
||||
check_to_execute = getattr(lib, check_name)
|
||||
check = check_to_execute()
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {self._provider.type.upper()} provider"
|
||||
)
|
||||
continue
|
||||
# Execute the check
|
||||
check_findings = execute(
|
||||
check,
|
||||
self._provider,
|
||||
custom_checks_metadata,
|
||||
output_options=None,
|
||||
)
|
||||
|
||||
# Store findings
|
||||
self._findings.extend(check_findings)
|
||||
|
||||
# Remove the executed check
|
||||
self._service_checks_to_execute[service].remove(check_name)
|
||||
if len(self._service_checks_to_execute[service]) == 0:
|
||||
self._service_checks_to_execute.pop(service, None)
|
||||
# Add the completed check
|
||||
if service not in self._service_checks_completed:
|
||||
self._service_checks_completed[service] = set()
|
||||
self._service_checks_completed[service].add(check_name)
|
||||
self._number_of_checks_completed += 1
|
||||
|
||||
# This should be done just once all the service's checks are completed
|
||||
# This metadata needs to get to the services not within the provider
|
||||
# since it is present in the Scan class
|
||||
self._provider.audit_metadata = update_audit_metadata(
|
||||
self._provider.audit_metadata,
|
||||
self.get_completed_services(),
|
||||
self.get_completed_checks(),
|
||||
)
|
||||
|
||||
findings = [
|
||||
Finding.generate_output(
|
||||
self._provider, finding, output_options=None
|
||||
)
|
||||
for finding in check_findings
|
||||
]
|
||||
|
||||
yield self.progress, findings
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {self._provider.type.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def get_completed_services(self) -> set[str]:
|
||||
"""
|
||||
get_completed_services returns the services that have been completed.
|
||||
|
||||
Example:
|
||||
get_completed_services() -> {"ec2", "s3"}
|
||||
"""
|
||||
return self._service_checks_completed.keys()
|
||||
|
||||
def get_completed_checks(self) -> set[str]:
|
||||
"""
|
||||
get_completed_checks returns the checks that have been completed.
|
||||
|
||||
Example:
|
||||
get_completed_checks() -> {"ec2_instance_public_ip", "s3_bucket_public"}
|
||||
"""
|
||||
completed_checks = set()
|
||||
for checks in self._service_checks_completed.values():
|
||||
completed_checks.update(checks)
|
||||
return completed_checks
|
||||
|
||||
|
||||
def get_service_name_from_check_name(check_name: str) -> str:
|
||||
"""
|
||||
get_service_name_from_check_name returns the service name for a given check name.
|
||||
|
||||
Example:
|
||||
get_service_name_from_check_name("ec2_instance_public") -> "ec2"
|
||||
"""
|
||||
return check_name.split("_")[0]
|
||||
|
||||
|
||||
def get_service_checks_to_execute(checks_to_execute: set[str]) -> dict[str, set[str]]:
|
||||
"""
|
||||
get_service_checks_to_execute returns a dictionary with the services and the checks to execute.
|
||||
|
||||
Example:
|
||||
get_service_checks_to_execute({"accessanalyzer_enabled", "ec2_instance_public_ip"})
|
||||
-> {"accessanalyzer": {"accessanalyzer_enabled"}, "ec2": {"ec2_instance_public_ip"}}
|
||||
"""
|
||||
service_checks_to_execute = dict()
|
||||
for check in checks_to_execute:
|
||||
# check -> accessanalyzer_enabled
|
||||
# service -> accessanalyzer
|
||||
service = get_service_name_from_check_name(check)
|
||||
if service not in service_checks_to_execute:
|
||||
service_checks_to_execute[service] = set()
|
||||
service_checks_to_execute[service].add(check)
|
||||
return service_checks_to_execute
|
||||
return all_findings
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Optional
|
||||
|
||||
from colorama import Style
|
||||
from detect_secrets import SecretsCollection
|
||||
from detect_secrets.settings import transient_settings
|
||||
from detect_secrets.settings import default_settings
|
||||
|
||||
from prowler.config.config import encoding_format_utf_8
|
||||
from prowler.lib.logger import logger
|
||||
@@ -80,96 +80,20 @@ def hash_sha512(string: str) -> str:
|
||||
return sha512(string.encode(encoding_format_utf_8)).hexdigest()[0:9]
|
||||
|
||||
|
||||
def detect_secrets_scan(
|
||||
data: str = None, file=None, excluded_secrets: list[str] = None
|
||||
) -> list[dict[str, str]]:
|
||||
"""detect_secrets_scan scans the data or file for secrets using the detect-secrets library.
|
||||
Args:
|
||||
data (str): The data to scan for secrets.
|
||||
file (str): The file to scan for secrets.
|
||||
excluded_secrets (list): A list of regex patterns to exclude from the scan.
|
||||
Returns:
|
||||
dict: The secrets found in the
|
||||
Raises:
|
||||
Exception: If an error occurs during the scan.
|
||||
Examples:
|
||||
>>> detect_secrets_scan(data="password=password")
|
||||
[{'filename': 'data', 'hashed_secret': 'f7c3bc1d808e04732adf679965ccc34ca7ae3441', 'is_verified': False, 'line_number': 1, 'type': 'Secret Keyword'}]
|
||||
>>> detect_secrets_scan(file="file.txt")
|
||||
{'file.txt': [{'filename': 'file.txt', 'hashed_secret': 'f7c3bc1d808e04732adf679965ccc34ca7ae3441', 'is_verified': False, 'line_number': 1, 'type': 'Secret Keyword'}]}
|
||||
"""
|
||||
try:
|
||||
if not file:
|
||||
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_data_file.write(bytes(data, encoding="raw_unicode_escape"))
|
||||
temp_data_file.close()
|
||||
def detect_secrets_scan(data):
|
||||
temp_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_data_file.write(bytes(data, encoding="raw_unicode_escape"))
|
||||
temp_data_file.close()
|
||||
|
||||
secrets = SecretsCollection()
|
||||
secrets = SecretsCollection()
|
||||
with default_settings():
|
||||
secrets.scan_file(temp_data_file.name)
|
||||
os.remove(temp_data_file.name)
|
||||
|
||||
settings = {
|
||||
"plugins_used": [
|
||||
{"name": "ArtifactoryDetector"},
|
||||
{"name": "AWSKeyDetector"},
|
||||
{"name": "AzureStorageKeyDetector"},
|
||||
{"name": "BasicAuthDetector"},
|
||||
{"name": "CloudantDetector"},
|
||||
{"name": "DiscordBotTokenDetector"},
|
||||
{"name": "GitHubTokenDetector"},
|
||||
{"name": "GitLabTokenDetector"},
|
||||
{"name": "Base64HighEntropyString", "limit": 6.0},
|
||||
{"name": "HexHighEntropyString", "limit": 3.0},
|
||||
{"name": "IbmCloudIamDetector"},
|
||||
{"name": "IbmCosHmacDetector"},
|
||||
{"name": "IPPublicDetector"},
|
||||
{"name": "JwtTokenDetector"},
|
||||
{"name": "KeywordDetector"},
|
||||
{"name": "MailchimpDetector"},
|
||||
{"name": "NpmDetector"},
|
||||
{"name": "OpenAIDetector"},
|
||||
{"name": "PrivateKeyDetector"},
|
||||
{"name": "PypiTokenDetector"},
|
||||
{"name": "SendGridDetector"},
|
||||
{"name": "SlackDetector"},
|
||||
{"name": "SoftlayerDetector"},
|
||||
{"name": "SquareOAuthDetector"},
|
||||
{"name": "StripeDetector"},
|
||||
{"name": "TelegramBotTokenDetector"},
|
||||
{"name": "TwilioKeyDetector"},
|
||||
],
|
||||
"filters_used": [
|
||||
{"path": "detect_secrets.filters.common.is_invalid_file"},
|
||||
{"path": "detect_secrets.filters.common.is_known_false_positive"},
|
||||
{"path": "detect_secrets.filters.heuristic.is_likely_id_string"},
|
||||
{"path": "detect_secrets.filters.heuristic.is_potential_secret"},
|
||||
],
|
||||
}
|
||||
if excluded_secrets and len(excluded_secrets) > 0:
|
||||
settings["filters_used"].append(
|
||||
{
|
||||
"path": "detect_secrets.filters.regex.should_exclude_line",
|
||||
"pattern": excluded_secrets,
|
||||
}
|
||||
)
|
||||
with transient_settings(settings):
|
||||
if file:
|
||||
secrets.scan_file(file)
|
||||
else:
|
||||
secrets.scan_file(temp_data_file.name)
|
||||
|
||||
if not file:
|
||||
os.remove(temp_data_file.name)
|
||||
|
||||
detect_secrets_output = secrets.json()
|
||||
|
||||
if detect_secrets_output:
|
||||
if file:
|
||||
return detect_secrets_output[file]
|
||||
else:
|
||||
return detect_secrets_output[temp_data_file.name]
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error scanning for secrets: {e}")
|
||||
detect_secrets_output = secrets.json()
|
||||
if detect_secrets_output:
|
||||
return detect_secrets_output[temp_data_file.name]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from datetime import datetime
|
||||
from re import fullmatch
|
||||
|
||||
from boto3 import client
|
||||
from boto3 import client, session
|
||||
from boto3.session import Session
|
||||
from botocore.config import Config
|
||||
from botocore.credentials import RefreshableCredentials
|
||||
from botocore.exceptions import ClientError, NoCredentialsError, ProfileNotFound
|
||||
from botocore.session import Session as BotocoreSession
|
||||
from botocore.session import get_session
|
||||
from colorama import Fore, Style
|
||||
from pytz import utc
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from prowler.config.config import aws_services_json_file, get_default_mute_file_path
|
||||
from prowler.lib.check.utils import list_modules, recover_checks_from_service
|
||||
from prowler.config.config import (
|
||||
aws_services_json_file,
|
||||
get_default_mute_file_path,
|
||||
load_and_validate_config_file,
|
||||
load_and_validate_fixer_config_file,
|
||||
)
|
||||
from prowler.lib.check.check import list_modules, recover_checks_from_service
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
|
||||
from prowler.providers.aws.config import (
|
||||
@@ -23,20 +28,6 @@ from prowler.providers.aws.config import (
|
||||
BOTO3_USER_AGENT_EXTRA,
|
||||
ROLE_SESSION_NAME,
|
||||
)
|
||||
from prowler.providers.aws.exceptions.exceptions import (
|
||||
AWSArgumentTypeValidationError,
|
||||
AWSAssumeRoleError,
|
||||
AWSClientError,
|
||||
AWSIAMRoleARNEmptyResource,
|
||||
AWSIAMRoleARNInvalidAccountID,
|
||||
AWSIAMRoleARNInvalidResourceType,
|
||||
AWSIAMRoleARNPartitionEmpty,
|
||||
AWSIAMRoleARNRegionNotEmtpy,
|
||||
AWSIAMRoleARNServiceNotIAMnorSTS,
|
||||
AWSNoCredentialsError,
|
||||
AWSProfileNotFoundError,
|
||||
AWSSetUpSessionError,
|
||||
)
|
||||
from prowler.providers.aws.lib.arn.arn import parse_iam_credentials_arn
|
||||
from prowler.providers.aws.lib.arn.models import ARN
|
||||
from prowler.providers.aws.lib.mutelist.mutelist import AWSMutelist
|
||||
@@ -52,9 +43,10 @@ from prowler.providers.aws.models import (
|
||||
AWSIdentityInfo,
|
||||
AWSMFAInfo,
|
||||
AWSOrganizationsInfo,
|
||||
AWSOutputOptions,
|
||||
AWSSession,
|
||||
)
|
||||
from prowler.providers.common.models import Audit_Metadata, Connection
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
@@ -67,60 +59,38 @@ class AwsProvider(Provider):
|
||||
_audit_config: dict
|
||||
_scan_unused_services: bool = False
|
||||
_enabled_regions: set = set()
|
||||
_output_options: AWSOutputOptions
|
||||
# TODO: this is not optional, enforce for all providers
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
retries_max_attempts: int = 3,
|
||||
role_arn: str = None,
|
||||
session_duration: int = None,
|
||||
external_id: str = None,
|
||||
role_session_name: str = None,
|
||||
mfa: bool = None,
|
||||
profile: str = None,
|
||||
regions: set = set(),
|
||||
organizations_role_arn: str = None,
|
||||
scan_unused_services: bool = None,
|
||||
resource_tags: list[str] = [],
|
||||
resource_arn: list[str] = [],
|
||||
audit_config: dict = {},
|
||||
fixer_config: dict = {},
|
||||
):
|
||||
"""
|
||||
Initializes the AWS provider.
|
||||
|
||||
Arguments:
|
||||
- retries_max_attempts: The maximum number of retries for the AWS client.
|
||||
- role_arn: The ARN of the IAM role to assume.
|
||||
- session_duration: The duration of the session in seconds.
|
||||
- external_id: The external ID to use when assuming the IAM role.
|
||||
- role_session_name: The name of the session when assuming the IAM role.
|
||||
- mfa: A boolean indicating whether MFA is enabled.
|
||||
- profile: The name of the AWS CLI profile to use.
|
||||
- regions: A set of regions to audit.
|
||||
- organizations_role_arn: The ARN of the AWS Organizations IAM role to assume.
|
||||
- scan_unused_services: A boolean indicating whether to scan unused services.
|
||||
- resource_tags: A list of tags to filter the resources to audit.
|
||||
- resource_arn: A list of ARNs of the resources to audit.
|
||||
- audit_config: The audit configuration.
|
||||
- fixer_config: The fixer configuration.
|
||||
|
||||
Raises:
|
||||
- ArgumentTypeError: If the input MFA ARN is invalid.
|
||||
- ArgumentTypeError: If the input session duration is invalid.
|
||||
- ArgumentTypeError: If the input external ID is invalid.
|
||||
- ArgumentTypeError: If the input role session name is invalid.
|
||||
|
||||
"""
|
||||
def __init__(self, arguments: Namespace):
|
||||
logger.info("Initializing AWS provider ...")
|
||||
######## Parse Arguments
|
||||
# Session
|
||||
aws_retries_max_attempts = getattr(arguments, "aws_retries_max_attempts", None)
|
||||
|
||||
# Assume Role
|
||||
input_role = getattr(arguments, "role", None)
|
||||
input_session_duration = getattr(arguments, "session_duration", None)
|
||||
input_external_id = getattr(arguments, "external_id", None)
|
||||
input_role_session_name = getattr(arguments, "role_session_name", None)
|
||||
|
||||
# MFA Configuration (false by default)
|
||||
input_mfa = getattr(arguments, "mfa", None)
|
||||
input_profile = getattr(arguments, "profile", None)
|
||||
input_regions = set(getattr(arguments, "region", set()))
|
||||
organizations_role_arn = getattr(arguments, "organizations_role", None)
|
||||
|
||||
# Set if unused services must be scanned
|
||||
scan_unused_services = getattr(arguments, "scan_unused_services", None)
|
||||
########
|
||||
|
||||
######## AWS Session
|
||||
logger.info("Generating original session ...")
|
||||
|
||||
# Configure the initial AWS Session using the local credentials: profile or environment variables
|
||||
aws_session = self.setup_session(mfa, profile)
|
||||
session_config = self.set_session_config(retries_max_attempts)
|
||||
aws_session = self.setup_session(input_mfa, input_profile, input_role)
|
||||
session_config = self.set_session_config(aws_retries_max_attempts)
|
||||
# Current session and the original session points to the same session object until we get a new one, if needed
|
||||
self._session = AWSSession(
|
||||
current_session=aws_session,
|
||||
@@ -133,15 +103,12 @@ class AwsProvider(Provider):
|
||||
# After the session is created, validate it
|
||||
logger.info("Validating credentials ...")
|
||||
sts_region = get_aws_region_for_sts(
|
||||
self.session.current_session.region_name, regions
|
||||
self.session.current_session.region_name, input_regions
|
||||
)
|
||||
|
||||
# Validate the credentials
|
||||
caller_identity = self.validate_credentials(
|
||||
session=self.session.current_session,
|
||||
aws_region=sts_region,
|
||||
caller_identity = validate_aws_credentials(
|
||||
self.session.current_session, sts_region
|
||||
)
|
||||
|
||||
logger.info("Credentials validated")
|
||||
########
|
||||
|
||||
@@ -152,24 +119,24 @@ class AwsProvider(Provider):
|
||||
# Set identity
|
||||
self._identity = self.set_identity(
|
||||
caller_identity=caller_identity,
|
||||
profile=profile,
|
||||
regions=regions,
|
||||
input_profile=input_profile,
|
||||
input_regions=input_regions,
|
||||
profile_region=profile_region,
|
||||
)
|
||||
########
|
||||
|
||||
######## AWS Session with Assume Role (if needed)
|
||||
if role_arn:
|
||||
if input_role:
|
||||
# Validate the input role
|
||||
valid_role_arn = parse_iam_credentials_arn(role_arn)
|
||||
valid_role_arn = parse_iam_credentials_arn(input_role)
|
||||
# Set assume IAM Role information
|
||||
assumed_role_information = AWSAssumeRoleInfo(
|
||||
role_arn=valid_role_arn,
|
||||
session_duration=session_duration,
|
||||
external_id=external_id,
|
||||
mfa_enabled=mfa,
|
||||
role_session_name=role_session_name,
|
||||
sts_region=sts_region,
|
||||
assumed_role_information = self.set_assumed_role_info(
|
||||
valid_role_arn,
|
||||
input_external_id,
|
||||
input_mfa,
|
||||
input_session_duration,
|
||||
input_role_session_name,
|
||||
sts_region,
|
||||
)
|
||||
# Assume the IAM Role
|
||||
logger.info(f"Assuming role: {assumed_role_information.role_arn.arn}")
|
||||
@@ -208,15 +175,14 @@ class AwsProvider(Provider):
|
||||
# Validate the input role
|
||||
valid_role_arn = parse_iam_credentials_arn(organizations_role_arn)
|
||||
# Set assume IAM Role information
|
||||
organizations_assumed_role_information = AWSAssumeRoleInfo(
|
||||
role_arn=valid_role_arn,
|
||||
session_duration=session_duration,
|
||||
external_id=external_id,
|
||||
mfa_enabled=mfa,
|
||||
role_session_name=role_session_name,
|
||||
sts_region=sts_region,
|
||||
organizations_assumed_role_information = self.set_assumed_role_info(
|
||||
valid_role_arn,
|
||||
input_external_id,
|
||||
input_mfa,
|
||||
input_session_duration,
|
||||
input_role_session_name,
|
||||
sts_region,
|
||||
)
|
||||
|
||||
# Assume the Organizations IAM Role
|
||||
logger.info(
|
||||
f"Assuming the AWS Organizations IAM Role: {organizations_assumed_role_information.role_arn.arn}"
|
||||
@@ -248,12 +214,12 @@ class AwsProvider(Provider):
|
||||
########
|
||||
|
||||
# Parse Scan Tags
|
||||
if resource_tags:
|
||||
self._audit_resources = self.get_tagged_resources(resource_tags)
|
||||
if getattr(arguments, "resource_tags", None):
|
||||
self._audit_resources = self.get_tagged_resources(arguments.resource_tags)
|
||||
|
||||
# Parse Input Resource ARNs
|
||||
if resource_arn:
|
||||
self._audit_resources = resource_arn
|
||||
if getattr(arguments, "resource_arn", None):
|
||||
self._audit_resources = arguments.resource_arn
|
||||
|
||||
# Get Enabled Regions
|
||||
self._enabled_regions = self.get_aws_enabled_regions(
|
||||
@@ -263,12 +229,18 @@ class AwsProvider(Provider):
|
||||
# Set ignore unused services
|
||||
self._scan_unused_services = scan_unused_services
|
||||
|
||||
# TODO: move this to the providers, pending for AWS, GCP, AZURE and K8s
|
||||
# Audit Config
|
||||
self._audit_config = audit_config
|
||||
# Fixer Config
|
||||
self._fixer_config = fixer_config
|
||||
|
||||
Provider.set_global_provider(self)
|
||||
self._audit_config = {}
|
||||
if hasattr(arguments, "config_file"):
|
||||
self._audit_config = load_and_validate_config_file(
|
||||
self._type, arguments.config_file
|
||||
)
|
||||
self._fixer_config = {}
|
||||
if hasattr(arguments, "fixer_config"):
|
||||
self._fixer_config = load_and_validate_fixer_config_file(
|
||||
self._type, arguments.fixer_config
|
||||
)
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
@@ -302,6 +274,17 @@ class AwsProvider(Provider):
|
||||
def fixer_config(self):
|
||||
return self._fixer_config
|
||||
|
||||
@property
|
||||
def output_options(self):
|
||||
return self._output_options
|
||||
|
||||
@output_options.setter
|
||||
def output_options(self, options: tuple):
|
||||
arguments, bulk_checks_metadata = options
|
||||
self._output_options = AWSOutputOptions(
|
||||
arguments, bulk_checks_metadata, self._identity
|
||||
)
|
||||
|
||||
@property
|
||||
def mutelist(self) -> AWSMutelist:
|
||||
"""
|
||||
@@ -400,8 +383,8 @@ class AwsProvider(Provider):
|
||||
def set_identity(
|
||||
self,
|
||||
caller_identity: AWSCallerIdentity,
|
||||
profile: str,
|
||||
regions: set,
|
||||
input_profile: str,
|
||||
input_regions: set,
|
||||
profile_region: str,
|
||||
) -> AWSIdentityInfo:
|
||||
logger.info(f"Original AWS Caller Identity UserId: {caller_identity.user_id}")
|
||||
@@ -414,20 +397,21 @@ class AwsProvider(Provider):
|
||||
user_id=caller_identity.user_id,
|
||||
partition=partition,
|
||||
identity_arn=caller_identity.arn.arn,
|
||||
profile=profile,
|
||||
profile=input_profile,
|
||||
profile_region=profile_region,
|
||||
audited_regions=regions,
|
||||
audited_regions=input_regions,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def setup_session(
|
||||
mfa: bool = False,
|
||||
profile: str = None,
|
||||
self,
|
||||
input_mfa: bool,
|
||||
input_profile: str,
|
||||
input_role: str = None,
|
||||
) -> Session:
|
||||
try:
|
||||
logger.info("Creating original session ...")
|
||||
if mfa:
|
||||
mfa_info = AwsProvider.input_role_mfa_token_and_code()
|
||||
if input_mfa and not input_role:
|
||||
mfa_info = self.__input_role_mfa_token_and_code__()
|
||||
# TODO: validate MFA ARN here
|
||||
get_session_token_arguments = {
|
||||
"SerialNumber": mfa_info.arn,
|
||||
@@ -445,51 +429,48 @@ class AwsProvider(Provider):
|
||||
aws_session_token=session_credentials["Credentials"][
|
||||
"SessionToken"
|
||||
],
|
||||
profile_name=profile,
|
||||
profile_name=input_profile,
|
||||
)
|
||||
else:
|
||||
return Session(
|
||||
profile_name=profile,
|
||||
profile_name=input_profile,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"AWSSetUpSessionError[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise AWSSetUpSessionError(
|
||||
original_exception=error,
|
||||
file=pathlib.Path(__file__).name,
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
def set_assumed_role_info(
|
||||
self,
|
||||
role_arn: str,
|
||||
input_external_id: str,
|
||||
input_mfa: str,
|
||||
session_duration: int,
|
||||
role_session_name: str,
|
||||
sts_region: str = AWS_STS_GLOBAL_ENDPOINT_REGION,
|
||||
) -> AWSAssumeRoleInfo:
|
||||
"""
|
||||
set_assumed_role_info returns a AWSAssumeRoleInfo object
|
||||
"""
|
||||
logger.info("Setting assume IAM Role information ...")
|
||||
return AWSAssumeRoleInfo(
|
||||
role_arn=role_arn,
|
||||
session_duration=session_duration,
|
||||
external_id=input_external_id,
|
||||
mfa_enabled=input_mfa,
|
||||
role_session_name=role_session_name,
|
||||
sts_region=sts_region,
|
||||
)
|
||||
|
||||
def setup_assumed_session(
|
||||
self,
|
||||
assumed_role_credentials: AWSCredentials,
|
||||
) -> Session:
|
||||
"""
|
||||
Sets up an assumed session using the provided assumed role credentials.
|
||||
|
||||
This method creates a new session with temporary credentials obtained by assuming an AWS IAM role.
|
||||
It uses the `RefreshableCredentials` class from the `botocore` library to manage the automatic
|
||||
refreshing of the assumed role credentials.
|
||||
|
||||
Args:
|
||||
assumed_role_credentials (AWSCredentials): The assumed role credentials.
|
||||
|
||||
Returns:
|
||||
Session: The assumed session.
|
||||
|
||||
Raises:
|
||||
Exception: If an error occurs during the setup process.
|
||||
|
||||
References:
|
||||
- `RefreshableCredentials` class in botocore:
|
||||
[GitHub](https://github.com/boto/botocore/blob/098cc255f81a25b852e1ecdeb7adebd94c7b1b73/botocore/credentials.py#L395)
|
||||
- AWS STS AssumeRole API:
|
||||
[AWS Documentation](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
|
||||
"""
|
||||
try:
|
||||
# From botocore we can use RefreshableCredentials class, which has an attribute (refresh_using)
|
||||
# that needs to be a method without arguments that retrieves a new set of fresh credentials
|
||||
# assuming the role again.
|
||||
# asuming the role again. -> https://github.com/boto/botocore/blob/098cc255f81a25b852e1ecdeb7adebd94c7b1b73/botocore/credentials.py#L395
|
||||
assumed_refreshable_credentials = RefreshableCredentials(
|
||||
access_key=assumed_role_credentials.aws_access_key_id,
|
||||
secret_key=assumed_role_credentials.aws_secret_access_key,
|
||||
@@ -500,10 +481,10 @@ class AwsProvider(Provider):
|
||||
)
|
||||
|
||||
# Here we need the botocore session since it needs to use refreshable credentials
|
||||
assumed_session = BotocoreSession()
|
||||
assumed_session = get_session()
|
||||
assumed_session._credentials = assumed_refreshable_credentials
|
||||
assumed_session.set_config_variable("region", self._identity.profile_region)
|
||||
return Session(
|
||||
return session.Session(
|
||||
profile_name=self._identity.profile,
|
||||
botocore_session=assumed_session,
|
||||
)
|
||||
@@ -511,7 +492,7 @@ class AwsProvider(Provider):
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
sys.exit(1)
|
||||
|
||||
# TODO: maybe this can be improved with botocore.credentials.DeferredRefreshableCredentials https://stackoverflow.com/a/75576540
|
||||
def refresh_credentials(self) -> dict:
|
||||
@@ -550,7 +531,7 @@ class AwsProvider(Provider):
|
||||
token=assume_role_response.aws_session_token,
|
||||
expiry_time=assume_role_response.expiration.isoformat(),
|
||||
)
|
||||
logger.info("Refreshed Credentials")
|
||||
logger.info(f"Refreshed Credentials: {refreshed_credentials}")
|
||||
|
||||
return refreshed_credentials
|
||||
|
||||
@@ -574,11 +555,11 @@ class AwsProvider(Provider):
|
||||
]
|
||||
# If -A is set, print Assumed Role ARN
|
||||
if (
|
||||
hasattr(self, "_assumed_role_configuration")
|
||||
and self._assumed_role_configuration.info.role_arn is not None
|
||||
hasattr(self, "_assumed_role")
|
||||
and self._assumed_role.info.role_arn is not None
|
||||
):
|
||||
report_lines.append(
|
||||
f"Assumed Role ARN: {Fore.YELLOW}[{self._assumed_role_configuration.info.role_arn.arn}]{Style.RESET_ALL}"
|
||||
f"Assumed Role ARN: {Fore.YELLOW}[{self._assumed_role.info.role_arn.arn}]{Style.RESET_ALL}"
|
||||
)
|
||||
report_title = (
|
||||
f"{Style.BRIGHT}Using the AWS credentials below:{Style.RESET_ALL}"
|
||||
@@ -706,12 +687,12 @@ class AwsProvider(Provider):
|
||||
audited_regions.add(region)
|
||||
return audited_regions
|
||||
|
||||
def get_tagged_resources(self, resource_tags: list[str]) -> list[str]:
|
||||
def get_tagged_resources(self, input_resource_tags: list[str]) -> list[str]:
|
||||
"""
|
||||
Returns a list of the resources that are going to be scanned based on the given input tags.
|
||||
|
||||
Parameters:
|
||||
- resource_tags: A list of strings representing the tags to filter the resources. Each string should be in the format "key=value".
|
||||
- input_resource_tags: A list of strings representing the tags to filter the resources. Each string should be in the format "key=value".
|
||||
|
||||
Returns:
|
||||
- A list of strings representing the ARNs (Amazon Resource Names) of the tagged resources.
|
||||
@@ -722,16 +703,16 @@ class AwsProvider(Provider):
|
||||
- The method paginates through the results of the 'get_resources' operation to retrieve all the tagged resources.
|
||||
|
||||
Example usage:
|
||||
resource_tags = ["Environment=Production", "Owner=John Doe"]
|
||||
tagged_resources = get_tagged_resources(resource_tags)
|
||||
input_resource_tags = ["Environment=Production", "Owner=John Doe"]
|
||||
tagged_resources = get_tagged_resources(input_resource_tags)
|
||||
"""
|
||||
try:
|
||||
resource_tags_values = []
|
||||
resource_tags = []
|
||||
tagged_resources = []
|
||||
for tag in resource_tags:
|
||||
for tag in input_resource_tags:
|
||||
key = tag.split("=")[0]
|
||||
value = tag.split("=")[1]
|
||||
resource_tags_values.append({"Key": key, "Values": [value]})
|
||||
resource_tags.append({"Key": key, "Values": [value]})
|
||||
# Get Resources with resource_tags for all regions
|
||||
for regional_client in self.generate_regional_clients(
|
||||
"resourcegroupstaggingapi"
|
||||
@@ -741,7 +722,7 @@ class AwsProvider(Provider):
|
||||
"get_resources"
|
||||
)
|
||||
for page in get_resources_paginator.paginate(
|
||||
TagFilters=resource_tags_values
|
||||
TagFilters=resource_tags
|
||||
):
|
||||
for resource in page["ResourceTagMappingList"]:
|
||||
tagged_resources.append(resource["ResourceARN"])
|
||||
@@ -749,13 +730,13 @@ class AwsProvider(Provider):
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
return tagged_resources
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
sys.exit(1)
|
||||
else:
|
||||
return tagged_resources
|
||||
|
||||
def get_default_region(self, service: str) -> str:
|
||||
"""get_default_region returns the default region based on the profile and audited service regions"""
|
||||
@@ -787,14 +768,13 @@ class AwsProvider(Provider):
|
||||
global_region = "aws-iso-global"
|
||||
return global_region
|
||||
|
||||
@staticmethod
|
||||
def input_role_mfa_token_and_code() -> AWSMFAInfo:
|
||||
def __input_role_mfa_token_and_code__(self) -> AWSMFAInfo:
|
||||
"""input_role_mfa_token_and_code ask for the AWS MFA ARN and TOTP and returns it."""
|
||||
mfa_ARN = input("Enter ARN of MFA: ")
|
||||
mfa_TOTP = input("Enter MFA code: ")
|
||||
return AWSMFAInfo(arn=mfa_ARN, totp=mfa_TOTP)
|
||||
|
||||
def set_session_config(self, retries_max_attempts: int) -> Config:
|
||||
def set_session_config(self, aws_retries_max_attempts: int) -> Config:
|
||||
"""
|
||||
set_session_config returns a botocore Config object with the Prowler user agent and the default retrier configuration if nothing is passed as argument
|
||||
"""
|
||||
@@ -803,11 +783,11 @@ class AwsProvider(Provider):
|
||||
retries={"max_attempts": 3, "mode": "standard"},
|
||||
user_agent_extra=BOTO3_USER_AGENT_EXTRA,
|
||||
)
|
||||
if retries_max_attempts:
|
||||
if aws_retries_max_attempts:
|
||||
# Create the new config
|
||||
config = Config(
|
||||
retries={
|
||||
"max_attempts": retries_max_attempts,
|
||||
"max_attempts": aws_retries_max_attempts,
|
||||
"mode": "standard",
|
||||
},
|
||||
)
|
||||
@@ -816,8 +796,8 @@ class AwsProvider(Provider):
|
||||
|
||||
return default_session_config
|
||||
|
||||
@staticmethod
|
||||
def assume_role(
|
||||
self,
|
||||
session: Session,
|
||||
assumed_role_info: AWSAssumeRoleInfo,
|
||||
) -> AWSCredentials:
|
||||
@@ -842,12 +822,10 @@ class AwsProvider(Provider):
|
||||
assume_role_arguments["ExternalId"] = assumed_role_info.external_id
|
||||
|
||||
if assumed_role_info.mfa_enabled:
|
||||
mfa_info = AwsProvider.input_role_mfa_token_and_code()
|
||||
mfa_info = self.__input_role_mfa_token_and_code__()
|
||||
assume_role_arguments["SerialNumber"] = mfa_info.arn
|
||||
assume_role_arguments["TokenCode"] = mfa_info.totp
|
||||
sts_client = AwsProvider.create_sts_session(
|
||||
session, assumed_role_info.sts_region
|
||||
)
|
||||
sts_client = create_sts_session(session, assumed_role_info.sts_region)
|
||||
assumed_credentials = sts_client.assume_role(**assume_role_arguments)
|
||||
# Convert the UTC datetime object to your local timezone
|
||||
credentials_expiration_local_time = (
|
||||
@@ -868,10 +846,7 @@ class AwsProvider(Provider):
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
raise AWSAssumeRoleError(
|
||||
original_exception=error,
|
||||
file=pathlib.Path(__file__).name,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
def get_aws_enabled_regions(self, current_session: Session) -> set:
|
||||
"""get_aws_enabled_regions returns a set of enabled AWS regions"""
|
||||
@@ -912,243 +887,7 @@ class AwsProvider(Provider):
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
|
||||
@staticmethod
|
||||
def validate_credentials(
|
||||
session: Session,
|
||||
aws_region: str,
|
||||
) -> AWSCallerIdentity:
|
||||
"""
|
||||
Validates the AWS credentials using the provided session and AWS region.
|
||||
Args:
|
||||
session (Session): The AWS session object.
|
||||
aws_region (str): The AWS region to validate the credentials.
|
||||
Returns:
|
||||
AWSCallerIdentity: An object containing the caller identity information.
|
||||
Raises:
|
||||
Exception: If an error occurs during the validation process.
|
||||
"""
|
||||
try:
|
||||
sts_client = AwsProvider.create_sts_session(session, aws_region)
|
||||
caller_identity = sts_client.get_caller_identity()
|
||||
# Include the region where the caller_identity has validated the credentials
|
||||
return AWSCallerIdentity(
|
||||
user_id=caller_identity.get("UserId"),
|
||||
account=caller_identity.get("Account"),
|
||||
arn=ARN(caller_identity.get("Arn")),
|
||||
region=aws_region,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
profile: str = None,
|
||||
aws_region: str = AWS_STS_GLOBAL_ENDPOINT_REGION,
|
||||
role_arn: str = None,
|
||||
role_session_name: str = ROLE_SESSION_NAME,
|
||||
session_duration: int = 3600,
|
||||
external_id: str = None,
|
||||
mfa_enabled: bool = False,
|
||||
raise_on_exception: bool = True,
|
||||
) -> Connection:
|
||||
"""
|
||||
Test the connection to AWS with one of the Boto3 credentials methods.
|
||||
|
||||
Args:
|
||||
profile (str): The AWS profile to use for the session.
|
||||
aws_region (str): The AWS region to validate the credentials in.
|
||||
role_arn (str): The ARN of the IAM role to assume.
|
||||
role_session_name (str): The name of the role session.
|
||||
session_duration (int): The duration of the assumed role session in seconds.
|
||||
external_id (str): The external ID to use when assuming the role.
|
||||
mfa_enabled (bool): Whether MFA (Multi-Factor Authentication) is enabled.
|
||||
raise_on_exception (bool): Whether to raise an exception if an error occurs.
|
||||
|
||||
Returns:
|
||||
Connection: An object tha contains the result of the test connection operation.
|
||||
- is_connected (bool): Indicates whether the validation was successful.
|
||||
- error (Exception): An exception object if an error occurs during the validation.
|
||||
|
||||
Raises:
|
||||
ClientError: If there is an error with the AWS client.
|
||||
ProfileNotFound: If the specified profile is not found.
|
||||
NoCredentialsError: If there are no AWS credentials found.
|
||||
ArgumentTypeError: If there is a validation error with the arguments.
|
||||
Exception: If there is an unexpected error.
|
||||
|
||||
Examples:
|
||||
>>> AwsProvider.test_connection(
|
||||
role_arn="arn:aws:iam::111122223333:role/ProwlerRole",
|
||||
external_id="67f7a641-ecb0-4f6d-921d-3587febd379c",
|
||||
raise_on_exception=False)
|
||||
)
|
||||
Connection(is_connected=True, Error=None)
|
||||
>>> AwsProvider.test_connection(profile="test", raise_on_exception=False)
|
||||
Connection(is_connected=True, Error=None)
|
||||
>>> AwsProvider.test_connection(profile="not-found", raise_on_exception=False))
|
||||
Connection(is_connected=False, Error=ProfileNotFound('The config profile (not-found) could not be found'))
|
||||
>>> AwsProvider.test_connection(raise_on_exception=False))
|
||||
Connection(is_connected=False, Error=NoCredentialsError('Unable to locate credentials'))
|
||||
"""
|
||||
try:
|
||||
session = AwsProvider.setup_session(mfa_enabled, profile)
|
||||
|
||||
if role_arn:
|
||||
session_duration = validate_session_duration(session_duration)
|
||||
role_session_name = validate_role_session_name(role_session_name)
|
||||
role_arn = parse_iam_credentials_arn(role_arn)
|
||||
assumed_role_information = AWSAssumeRoleInfo(
|
||||
role_arn=role_arn,
|
||||
session_duration=session_duration,
|
||||
external_id=external_id,
|
||||
mfa_enabled=mfa_enabled,
|
||||
role_session_name=role_session_name,
|
||||
)
|
||||
assumed_role_credentials = AwsProvider.assume_role(
|
||||
session,
|
||||
assumed_role_information,
|
||||
)
|
||||
session = Session(
|
||||
aws_access_key_id=assumed_role_credentials.aws_access_key_id,
|
||||
aws_secret_access_key=assumed_role_credentials.aws_secret_access_key,
|
||||
aws_session_token=assumed_role_credentials.aws_session_token,
|
||||
region_name=aws_region,
|
||||
profile_name=profile,
|
||||
)
|
||||
|
||||
sts_client = AwsProvider.create_sts_session(session, aws_region)
|
||||
_ = sts_client.get_caller_identity()
|
||||
return Connection(
|
||||
is_connected=True,
|
||||
)
|
||||
|
||||
except AWSSetUpSessionError as setup_session_error:
|
||||
logger.error(str(setup_session_error))
|
||||
if raise_on_exception:
|
||||
raise setup_session_error
|
||||
return Connection(error=setup_session_error)
|
||||
|
||||
except AWSArgumentTypeValidationError as validation_error:
|
||||
logger.error(str(validation_error))
|
||||
if raise_on_exception:
|
||||
raise validation_error
|
||||
return Connection(error=validation_error)
|
||||
|
||||
except AWSIAMRoleARNRegionNotEmtpy as arn_region_not_empty_error:
|
||||
logger.error(str(arn_region_not_empty_error))
|
||||
if raise_on_exception:
|
||||
raise arn_region_not_empty_error
|
||||
return Connection(error=arn_region_not_empty_error)
|
||||
|
||||
except AWSIAMRoleARNPartitionEmpty as arn_partition_empty_error:
|
||||
logger.error(str(arn_partition_empty_error))
|
||||
if raise_on_exception:
|
||||
raise arn_partition_empty_error
|
||||
return Connection(error=arn_partition_empty_error)
|
||||
|
||||
except AWSIAMRoleARNServiceNotIAMnorSTS as arn_service_not_iam_sts_error:
|
||||
logger.error(str(arn_service_not_iam_sts_error))
|
||||
if raise_on_exception:
|
||||
raise arn_service_not_iam_sts_error
|
||||
return Connection(error=arn_service_not_iam_sts_error)
|
||||
|
||||
except AWSIAMRoleARNInvalidAccountID as arn_invalid_account_id_error:
|
||||
logger.error(str(arn_invalid_account_id_error))
|
||||
if raise_on_exception:
|
||||
raise arn_invalid_account_id_error
|
||||
return Connection(error=arn_invalid_account_id_error)
|
||||
|
||||
except AWSIAMRoleARNInvalidResourceType as arn_invalid_resource_type_error:
|
||||
logger.error(str(arn_invalid_resource_type_error))
|
||||
if raise_on_exception:
|
||||
raise arn_invalid_resource_type_error
|
||||
return Connection(error=arn_invalid_resource_type_error)
|
||||
|
||||
except AWSIAMRoleARNEmptyResource as arn_empty_resource_error:
|
||||
logger.error(str(arn_empty_resource_error))
|
||||
if raise_on_exception:
|
||||
raise arn_empty_resource_error
|
||||
return Connection(error=arn_empty_resource_error)
|
||||
|
||||
except AWSAssumeRoleError as assume_role_error:
|
||||
logger.error(str(assume_role_error))
|
||||
if raise_on_exception:
|
||||
raise assume_role_error
|
||||
return Connection(error=assume_role_error)
|
||||
|
||||
except ClientError as client_error:
|
||||
logger.error(
|
||||
f"AWSClientError[{client_error.__traceback__.tb_lineno}]: {client_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise AWSClientError(
|
||||
file=os.path.basename(__file__), original_exception=client_error
|
||||
) from client_error
|
||||
return Connection(error=client_error)
|
||||
|
||||
except ProfileNotFound as profile_not_found_error:
|
||||
logger.error(
|
||||
f"AWSProfileNotFoundError[{profile_not_found_error.__traceback__.tb_lineno}]: {profile_not_found_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise AWSProfileNotFoundError(
|
||||
file=os.path.basename(__file__),
|
||||
original_exception=profile_not_found_error,
|
||||
) from profile_not_found_error
|
||||
return Connection(error=profile_not_found_error)
|
||||
|
||||
except NoCredentialsError as no_credentials_error:
|
||||
logger.error(
|
||||
f"AWSNoCredentialsError[{no_credentials_error.__traceback__.tb_lineno}]: {no_credentials_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise AWSNoCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
original_exception=no_credentials_error,
|
||||
) from no_credentials_error
|
||||
return Connection(error=no_credentials_error)
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
|
||||
@staticmethod
|
||||
def create_sts_session(
|
||||
session: Session, aws_region: str = AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
) -> Session.client:
|
||||
"""
|
||||
Create an STS session client.
|
||||
|
||||
Parameters:
|
||||
- session (session.Session): The AWS session object.
|
||||
- aws_region (str): The AWS region to use for the session.
|
||||
|
||||
Returns:
|
||||
- session.Session.client: The STS session client.
|
||||
|
||||
Example:
|
||||
session = boto3.session.Session()
|
||||
sts_client = create_sts_session(session, 'us-west-2')
|
||||
"""
|
||||
try:
|
||||
sts_endpoint_url = (
|
||||
f"https://sts.{aws_region}.amazonaws.com"
|
||||
if not aws_region.startswith("cn-")
|
||||
else f"https://sts.{aws_region}.amazonaws.com.cn"
|
||||
)
|
||||
return session.client("sts", aws_region, endpoint_url=sts_endpoint_url)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def read_aws_regions_file() -> dict:
|
||||
@@ -1188,9 +927,35 @@ def get_aws_available_regions() -> set:
|
||||
|
||||
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
|
||||
# TODO: rename to validate_credentials
|
||||
def validate_aws_credentials(
|
||||
session: Session,
|
||||
aws_region: str,
|
||||
) -> AWSCallerIdentity:
|
||||
"""
|
||||
validate_aws_credentials returns the get_caller_identity() answer, exits if something exception is raised.
|
||||
"""
|
||||
try:
|
||||
validate_credentials_client = create_sts_session(session, aws_region)
|
||||
caller_identity = validate_credentials_client.get_caller_identity()
|
||||
# Include the region where the caller_identity has validated the credentials
|
||||
return AWSCallerIdentity(
|
||||
user_id=caller_identity.get("UserId"),
|
||||
account=caller_identity.get("Account"),
|
||||
arn=ARN(caller_identity.get("Arn")),
|
||||
region=aws_region,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def get_aws_region_for_sts(session_region: str, input_regions: set[str]) -> str:
|
||||
# If there is no region passed with -f/--region/--filter-region
|
||||
if regions is None or len(regions) == 0:
|
||||
if input_regions is None or len(input_regions) == 0:
|
||||
# If you have a region configured in your AWS config or credentials file
|
||||
if session_region is not None:
|
||||
aws_region = session_region
|
||||
@@ -1200,57 +965,38 @@ def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
|
||||
aws_region = AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
else:
|
||||
# Get the first region passed to the -f/--region
|
||||
aws_region = list(regions)[0]
|
||||
aws_region = list(input_regions)[0]
|
||||
|
||||
return aws_region
|
||||
|
||||
|
||||
# TODO: this duplicates the provider arguments validation library
|
||||
def validate_session_duration(duration: int) -> int:
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def create_sts_session(
|
||||
session: session.Session, aws_region: str = AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
) -> session.Session.client:
|
||||
"""
|
||||
validate_session_duration validates that the AWS STS Assume Role Session Duration is between 900 and 43200 seconds.
|
||||
Create an STS session client.
|
||||
|
||||
Args:
|
||||
duration (int): The session duration in seconds.
|
||||
Parameters:
|
||||
- session (session.Session): The AWS session object.
|
||||
- aws_region (str): The AWS region to use for the session.
|
||||
|
||||
Returns:
|
||||
int: The validated session duration.
|
||||
- session.Session.client: The STS session client.
|
||||
|
||||
Raises:
|
||||
ArgumentTypeError: If the session duration is not within the valid range.
|
||||
Example:
|
||||
session = boto3.session.Session()
|
||||
sts_client = create_sts_session(session, 'us-west-2')
|
||||
"""
|
||||
duration = int(duration)
|
||||
# Since the range(i,j) goes from i to j-1 we have to j+1
|
||||
if duration not in range(900, 43201):
|
||||
raise AWSArgumentTypeValidationError(
|
||||
message="Session Duration must be between 900 and 43200 seconds.",
|
||||
file=os.path.basename(__file__),
|
||||
try:
|
||||
sts_endpoint_url = (
|
||||
f"https://sts.{aws_region}.amazonaws.com"
|
||||
if "cn-" not in aws_region
|
||||
else f"https://sts.{aws_region}.amazonaws.com.cn"
|
||||
)
|
||||
else:
|
||||
return duration
|
||||
|
||||
|
||||
# TODO: this duplicates the provider arguments validation library
|
||||
def validate_role_session_name(session_name) -> str:
|
||||
"""
|
||||
Validates that the role session name is valid.
|
||||
|
||||
Args:
|
||||
session_name (str): The role session name to be validated.
|
||||
|
||||
Returns:
|
||||
str: The validated role session name.
|
||||
|
||||
Raises:
|
||||
ArgumentTypeError: If the role session name is invalid.
|
||||
|
||||
Documentation:
|
||||
- AWS STS AssumeRole API: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
"""
|
||||
if fullmatch(r"[\w+=,.@-]{2,64}", session_name):
|
||||
return session_name
|
||||
else:
|
||||
raise AWSArgumentTypeValidationError(
|
||||
file=os.path.basename(__file__),
|
||||
message="Role Session Name must be between 2 and 64 characters and may contain alphanumeric characters, periods, hyphens, and underscores.",
|
||||
return session.client("sts", aws_region, endpoint_url=sts_endpoint_url)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -96,7 +95,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -140,7 +138,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -174,8 +171,6 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-south-1",
|
||||
"ap-southeast-2",
|
||||
"eu-west-2",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
@@ -332,7 +327,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -452,7 +446,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -496,7 +489,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -617,9 +609,7 @@
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
@@ -628,7 +618,6 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
@@ -670,10 +659,8 @@
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -822,7 +809,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -866,7 +852,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -990,7 +975,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1034,7 +1018,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1089,7 +1072,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1219,7 +1201,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1280,28 +1261,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"bedrock-runtime": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"billingconductor": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -1607,7 +1566,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1734,7 +1692,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1778,7 +1735,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1876,7 +1832,6 @@
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -1909,7 +1864,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1962,7 +1916,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -2225,6 +2178,27 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"codestar": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"codestar-connections": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -2520,7 +2494,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -2846,7 +2819,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3036,7 +3008,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3095,7 +3066,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3139,7 +3109,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3172,7 +3141,6 @@
|
||||
"docdb": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
@@ -3183,7 +3151,6 @@
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -3299,7 +3266,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3343,7 +3309,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3387,7 +3352,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3431,7 +3395,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3475,7 +3438,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3529,7 +3491,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3573,7 +3534,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3617,7 +3577,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3661,7 +3620,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3682,10 +3640,7 @@
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"elastic-inference": {
|
||||
@@ -3716,7 +3671,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3813,7 +3767,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3857,7 +3810,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3901,7 +3853,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4040,7 +3991,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4084,7 +4034,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4128,7 +4077,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4189,7 +4137,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4282,7 +4229,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4949,6 +4895,15 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"honeycode": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -4963,7 +4918,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5093,7 +5047,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5771,7 +5724,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5887,7 +5839,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5974,7 +5925,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -6308,7 +6258,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -6579,7 +6528,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -6818,7 +6766,6 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-4",
|
||||
@@ -6828,7 +6775,6 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -7203,11 +7149,9 @@
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -7419,7 +7363,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7443,10 +7386,7 @@
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"omics": {
|
||||
@@ -7478,7 +7418,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7526,9 +7465,7 @@
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-west-1"
|
||||
]
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"opsworks": {
|
||||
@@ -7619,7 +7556,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7806,7 +7742,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8021,7 +7956,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8081,16 +8015,6 @@
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"qapps": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"us-east-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"qbusiness": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -8101,15 +8025,6 @@
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"qdeveloper": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"us-east-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"qldb": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -8193,7 +8108,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8237,7 +8151,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8281,7 +8194,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8356,7 +8268,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8400,7 +8311,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8433,22 +8343,11 @@
|
||||
"redshift-serverless": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -8584,7 +8483,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8628,7 +8526,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8770,7 +8667,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8797,50 +8693,6 @@
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"route53-application-recovery-controller": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"route53-recovery-readiness": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -8859,46 +8711,6 @@
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"route53profiles": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"route53resolver": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -8913,7 +8725,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -8991,7 +8802,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -9284,7 +9094,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -9415,7 +9224,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -9459,7 +9267,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -9563,7 +9370,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -9689,7 +9495,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -9968,7 +9773,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10012,7 +9816,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10056,7 +9859,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10143,22 +9945,16 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -10227,7 +10023,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10314,7 +10109,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10357,7 +10151,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10412,7 +10205,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10456,7 +10248,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10558,16 +10349,9 @@
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-central-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
@@ -10701,7 +10485,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10771,7 +10554,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10935,7 +10717,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10968,8 +10749,6 @@
|
||||
"vpc-lattice": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
@@ -10978,11 +10757,9 @@
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -11007,7 +10784,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -11350,7 +11126,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
class AWSBaseException(ProwlerException):
|
||||
"""Base class for AWS errors."""
|
||||
|
||||
AWS_ERROR_CODES = {
|
||||
(1902, "AWSClientError"): {
|
||||
"message": "AWS ClientError occurred",
|
||||
"remediation": "Check your AWS client configuration and permissions.",
|
||||
},
|
||||
(1903, "AWSProfileNotFoundError"): {
|
||||
"message": "AWS Profile not found",
|
||||
"remediation": "Ensure the AWS profile is correctly configured, please visit https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html",
|
||||
},
|
||||
(1904, "AWSNoCredentialsError"): {
|
||||
"message": "No AWS credentials found",
|
||||
"remediation": "Verify that AWS credentials are properly set up, please visit https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/aws/authentication/ and https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-configure.html",
|
||||
},
|
||||
(1905, "AWSArgumentTypeValidationError"): {
|
||||
"message": "AWS argument type validation error",
|
||||
"remediation": "Check the provided argument types specific to AWS and ensure they meet the required format. For session duration check: https://docs.aws.amazon.com/singlesignon/latest/userguide/howtosessionduration.html and for role session name check: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-session-name",
|
||||
},
|
||||
(1906, "AWSSetUpSessionError"): {
|
||||
"message": "AWS session setup error",
|
||||
"remediation": "Check the AWS session setup and ensure it is properly configured, please visit https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html and check if the provided profile has the necessary permissions.",
|
||||
},
|
||||
(1907, "AWSIAMRoleARNRegionNotEmtpy"): {
|
||||
"message": "AWS IAM Role ARN region is not empty",
|
||||
"remediation": "Check the AWS IAM Role ARN region and ensure it is empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1908, "AWSIAMRoleARNPartitionEmpty"): {
|
||||
"message": "AWS IAM Role ARN partition is empty",
|
||||
"remediation": "Check the AWS IAM Role ARN partition and ensure it is not empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1909, "AWSIAMRoleARNMissingFields"): {
|
||||
"message": "AWS IAM Role ARN missing fields",
|
||||
"remediation": "Check the AWS IAM Role ARN and ensure all required fields are present, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1910, "AWSIAMRoleARNServiceNotIAMnorSTS"): {
|
||||
"message": "AWS IAM Role ARN service is not IAM nor STS",
|
||||
"remediation": "Check the AWS IAM Role ARN service and ensure it is either IAM or STS, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1911, "AWSIAMRoleARNInvalidAccountID"): {
|
||||
"message": "AWS IAM Role ARN account ID is invalid",
|
||||
"remediation": "Check the AWS IAM Role ARN account ID and ensure it is a valid 12-digit number, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1912, "AWSIAMRoleARNInvalidResourceType"): {
|
||||
"message": "AWS IAM Role ARN resource type is invalid",
|
||||
"remediation": "Check the AWS IAM Role ARN resource type and ensure it is valid, resources types are: role, user, assumed-role, root, federated-user, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1913, "AWSIAMRoleARNEmptyResource"): {
|
||||
"message": "AWS IAM Role ARN resource is empty",
|
||||
"remediation": "Check the AWS IAM Role ARN resource and ensure it is not empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1914, "AWSAssumeRoleError"): {
|
||||
"message": "AWS assume role error",
|
||||
"remediation": "Check the AWS assume role configuration and ensure it is properly set up, please visit https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/aws/role-assumption/ and https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-session-name",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
error_info = self.AWS_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code,
|
||||
provider="AWS",
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
)
|
||||
|
||||
|
||||
class AWSCredentialsError(AWSBaseException):
|
||||
"""Base class for AWS credentials errors."""
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
super().__init__(code, file, original_exception, message)
|
||||
|
||||
|
||||
class AWSClientError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1902, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSProfileNotFoundError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1903, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSNoCredentialsError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1904, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSArgumentTypeValidationError(AWSBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1905, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSSetUpSessionError(AWSBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1906, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSRoleArnError(AWSBaseException):
|
||||
"""Base class for AWS role ARN errors."""
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
super().__init__(code, file, original_exception, message)
|
||||
|
||||
|
||||
class AWSIAMRoleARNRegionNotEmtpy(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1907, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNPartitionEmpty(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1908, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNMissingFields(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1909, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNServiceNotIAMnorSTS(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1910, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNInvalidAccountID(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1911, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNInvalidResourceType(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1912, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNEmptyResource(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1913, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSAssumeRoleError(AWSBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1914, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
@@ -168,40 +168,13 @@ def init_parser(self):
|
||||
)
|
||||
|
||||
|
||||
def validate_session_duration(session_duration: int) -> int:
|
||||
"""validate_session_duration validates that the input session_duration is valid"""
|
||||
duration = int(session_duration)
|
||||
def validate_session_duration(duration):
|
||||
"""validate_session_duration validates that the AWS STS Assume Role Session Duration is between 900 and 43200 seconds."""
|
||||
duration = int(duration)
|
||||
# Since the range(i,j) goes from i to j-1 we have to j+1
|
||||
if duration not in range(900, 43201):
|
||||
raise ArgumentTypeError(
|
||||
"Session duration must be between 900 and 43200 seconds"
|
||||
)
|
||||
else:
|
||||
return duration
|
||||
|
||||
|
||||
def validate_role_session_name(session_name) -> str:
|
||||
"""
|
||||
Validates that the role session name is valid.
|
||||
|
||||
Args:
|
||||
session_name (str): The role session name to be validated.
|
||||
|
||||
Returns:
|
||||
str: The validated role session name.
|
||||
|
||||
Raises:
|
||||
ArgumentTypeError: If the role session name is invalid.
|
||||
|
||||
Documentation:
|
||||
- AWS STS AssumeRole API: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
"""
|
||||
if fullmatch(r"[\w+=,.@-]{2,64}", session_name):
|
||||
return session_name
|
||||
else:
|
||||
raise ArgumentTypeError(
|
||||
"Role session name must be between 2 and 64 characters long and may contain alphanumeric characters, hyphens, underscores, plus signs, equal signs, commas, periods, at signs, and tildes."
|
||||
)
|
||||
raise ArgumentTypeError("Session duration must be between 900 and 43200")
|
||||
return duration
|
||||
|
||||
|
||||
def validate_arguments(arguments: Namespace) -> tuple[bool, str]:
|
||||
@@ -222,7 +195,7 @@ def validate_arguments(arguments: Namespace) -> tuple[bool, str]:
|
||||
return (True, "")
|
||||
|
||||
|
||||
def validate_bucket(bucket_name: str) -> str:
|
||||
def validate_bucket(bucket_name):
|
||||
"""validate_bucket validates that the input bucket_name is valid"""
|
||||
if search("(?!(^xn--|.+-s3alias$))^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$", bucket_name):
|
||||
return bucket_name
|
||||
@@ -230,3 +203,16 @@ def validate_bucket(bucket_name: str) -> str:
|
||||
raise ArgumentTypeError(
|
||||
"Bucket name must be valid (https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html)"
|
||||
)
|
||||
|
||||
|
||||
def validate_role_session_name(session_name):
|
||||
"""
|
||||
validates that the role session name is valid
|
||||
Documentation: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
"""
|
||||
if fullmatch(r"[\w+=,.@-]{2,64}", session_name):
|
||||
return session_name
|
||||
else:
|
||||
raise ArgumentTypeError(
|
||||
"Role Session Name must be 2-64 characters long and consist only of upper- and lower-case alphanumeric characters with no spaces. You can also include underscores or any of the following characters: =,.@-"
|
||||
)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import os
|
||||
import re
|
||||
from argparse import ArgumentTypeError
|
||||
|
||||
from prowler.providers.aws.exceptions.exceptions import (
|
||||
AWSIAMRoleARNEmptyResource,
|
||||
AWSIAMRoleARNInvalidAccountID,
|
||||
AWSIAMRoleARNInvalidResourceType,
|
||||
AWSIAMRoleARNPartitionEmpty,
|
||||
AWSIAMRoleARNRegionNotEmtpy,
|
||||
AWSIAMRoleARNServiceNotIAMnorSTS,
|
||||
from prowler.providers.aws.lib.arn.error import (
|
||||
RoleArnParsingEmptyResource,
|
||||
RoleArnParsingIAMRegionNotEmpty,
|
||||
RoleArnParsingInvalidAccountID,
|
||||
RoleArnParsingInvalidResourceType,
|
||||
RoleArnParsingPartitionEmpty,
|
||||
RoleArnParsingServiceNotIAMnorSTS,
|
||||
)
|
||||
from prowler.providers.aws.lib.arn.models import ARN
|
||||
|
||||
@@ -25,7 +24,7 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
arn_parsed = ARN(arn)
|
||||
# First check if region is empty (in IAM ARN's region is always empty)
|
||||
if arn_parsed.region:
|
||||
raise AWSIAMRoleARNRegionNotEmtpy(file=os.path.basename(__file__))
|
||||
raise RoleArnParsingIAMRegionNotEmpty
|
||||
else:
|
||||
# check if needed fields are filled:
|
||||
# - partition
|
||||
@@ -34,15 +33,15 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
# - resource_type
|
||||
# - resource
|
||||
if arn_parsed.partition is None or arn_parsed.partition == "":
|
||||
raise AWSIAMRoleARNPartitionEmpty(file=os.path.basename(__file__))
|
||||
raise RoleArnParsingPartitionEmpty
|
||||
elif arn_parsed.service != "iam" and arn_parsed.service != "sts":
|
||||
raise AWSIAMRoleARNServiceNotIAMnorSTS(file=os.path.basename(__file__))
|
||||
raise RoleArnParsingServiceNotIAMnorSTS
|
||||
elif (
|
||||
arn_parsed.account_id is None
|
||||
or len(arn_parsed.account_id) != 12
|
||||
or not arn_parsed.account_id.isnumeric()
|
||||
):
|
||||
raise AWSIAMRoleARNInvalidAccountID(file=os.path.basename(__file__))
|
||||
raise RoleArnParsingInvalidAccountID
|
||||
elif (
|
||||
arn_parsed.resource_type != "role"
|
||||
and arn_parsed.resource_type != "user"
|
||||
@@ -50,9 +49,9 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
and arn_parsed.resource_type != "root"
|
||||
and arn_parsed.resource_type != "federated-user"
|
||||
):
|
||||
raise AWSIAMRoleARNInvalidResourceType(file=os.path.basename(__file__))
|
||||
raise RoleArnParsingInvalidResourceType
|
||||
elif arn_parsed.resource == "":
|
||||
raise AWSIAMRoleARNEmptyResource(file=os.path.basename(__file__))
|
||||
raise RoleArnParsingEmptyResource
|
||||
else:
|
||||
return arn_parsed
|
||||
|
||||
|
||||
49
prowler/providers/aws/lib/arn/error.py
Normal file
49
prowler/providers/aws/lib/arn/error.py
Normal file
@@ -0,0 +1,49 @@
|
||||
class RoleArnParsingFailedMissingFields(Exception):
|
||||
# The ARN contains a number of fields different than six separated by :"
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN contains an invalid number of fields separated by : or it does not start by arn, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RoleArnParsingIAMRegionNotEmpty(Exception):
|
||||
# The ARN contains a non-empty value for region, since it is an IAM ARN is not valid
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN contains a non-empty value for region, since it is an IAM ARN is not valid, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RoleArnParsingPartitionEmpty(Exception):
|
||||
# The ARN contains an empty value for partition
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN does not contain a value for partition, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RoleArnParsingServiceNotIAMnorSTS(Exception):
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN contains a value for service distinct than IAM or STS, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RoleArnParsingServiceNotSTS(Exception):
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN contains a value for service distinct than STS, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RoleArnParsingInvalidAccountID(Exception):
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN contains a value for account id empty or invalid, a valid account id must be composed of 12 numbers, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RoleArnParsingInvalidResourceType(Exception):
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN contains a value for resource type different than role, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RoleArnParsingEmptyResource(Exception):
|
||||
def __init__(self):
|
||||
self.message = "The assumed role ARN does not contain a value for resource, please input a valid ARN"
|
||||
super().__init__(self.message)
|
||||
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.providers.aws.exceptions.exceptions import AWSIAMRoleARNMissingFields
|
||||
from prowler.providers.aws.lib.arn.error import RoleArnParsingFailedMissingFields
|
||||
|
||||
|
||||
class ARN(BaseModel):
|
||||
@@ -19,7 +18,7 @@ class ARN(BaseModel):
|
||||
# Validate the ARN
|
||||
## Check that arn starts with arn
|
||||
if not arn.startswith("arn:"):
|
||||
raise AWSIAMRoleARNMissingFields(file=os.path.basename(__file__))
|
||||
raise RoleArnParsingFailedMissingFields
|
||||
## Retrieve fields
|
||||
arn_elements = arn.split(":", 5)
|
||||
data = {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
def is_condition_block_restrictive(
|
||||
condition_statement: dict,
|
||||
source_account: str,
|
||||
is_cross_account_allowed=False,
|
||||
):
|
||||
"""
|
||||
is_condition_block_restrictive parses the IAM Condition policy block and, by default, returns True if the source_account passed as argument is within, False if not.
|
||||
|
||||
If argument is_cross_account_allowed is True it tests if the Condition block includes any of the operators allowlisted returning True if does, False if not.
|
||||
|
||||
|
||||
@param condition_statement: dict with an IAM Condition block, e.g.:
|
||||
{
|
||||
"StringLike": {
|
||||
"AWS:SourceAccount": 111122223333
|
||||
}
|
||||
}
|
||||
|
||||
@param source_account: str with a 12-digit AWS Account number, e.g.: 111122223333
|
||||
|
||||
@param is_cross_account_allowed: bool to allow cross-account access, e.g.: True
|
||||
|
||||
"""
|
||||
is_condition_valid = False
|
||||
|
||||
# The conditions must be defined in lowercase since the context key names are not case-sensitive.
|
||||
# For example, including the aws:SourceAccount context key is equivalent to testing for AWS:SourceAccount
|
||||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
|
||||
valid_condition_options = {
|
||||
"StringEquals": [
|
||||
"aws:sourceaccount",
|
||||
"aws:sourceowner",
|
||||
"s3:resourceaccount",
|
||||
"aws:principalaccount",
|
||||
"aws:resourceaccount",
|
||||
"aws:sourcearn",
|
||||
"aws:sourcevpc",
|
||||
"aws:sourcevpce",
|
||||
],
|
||||
"StringLike": [
|
||||
"aws:sourceaccount",
|
||||
"aws:sourceowner",
|
||||
"aws:sourcearn",
|
||||
"aws:principalarn",
|
||||
"aws:resourceaccount",
|
||||
"aws:principalaccount",
|
||||
"aws:sourcevpc",
|
||||
"aws:sourcevpce",
|
||||
],
|
||||
"ArnLike": ["aws:sourcearn", "aws:principalarn"],
|
||||
"ArnEquals": ["aws:sourcearn", "aws:principalarn"],
|
||||
}
|
||||
|
||||
for condition_operator, condition_operator_key in valid_condition_options.items():
|
||||
if condition_operator in condition_statement:
|
||||
for value in condition_operator_key:
|
||||
# We need to transform the condition_statement into lowercase
|
||||
condition_statement[condition_operator] = {
|
||||
k.lower(): v
|
||||
for k, v in condition_statement[condition_operator].items()
|
||||
}
|
||||
|
||||
if value in condition_statement[condition_operator]:
|
||||
# values are a list
|
||||
if isinstance(
|
||||
condition_statement[condition_operator][value],
|
||||
list,
|
||||
):
|
||||
is_condition_key_restrictive = True
|
||||
# if cross account is not allowed check for each condition block looking for accounts
|
||||
# different than default
|
||||
if not is_cross_account_allowed:
|
||||
# if there is an arn/account without the source account -> we do not consider it safe
|
||||
# here by default we assume is true and look for false entries
|
||||
for item in condition_statement[condition_operator][value]:
|
||||
if source_account not in item:
|
||||
is_condition_key_restrictive = False
|
||||
break
|
||||
|
||||
if is_condition_key_restrictive:
|
||||
is_condition_valid = True
|
||||
|
||||
# value is a string
|
||||
elif isinstance(
|
||||
condition_statement[condition_operator][value],
|
||||
str,
|
||||
):
|
||||
if is_cross_account_allowed:
|
||||
is_condition_valid = True
|
||||
else:
|
||||
if (
|
||||
source_account
|
||||
in condition_statement[condition_operator][value]
|
||||
):
|
||||
is_condition_valid = True
|
||||
|
||||
return is_condition_valid
|
||||
|
||||
|
||||
def is_condition_block_restrictive_organization(
|
||||
condition_statement: dict,
|
||||
):
|
||||
"""
|
||||
is_condition_block_restrictive_organization parses the IAM Condition policy block and returns True if the condition_statement is restrictive for the organization, False if not.
|
||||
|
||||
@param condition_statement: dict with an IAM Condition block, e.g.:
|
||||
{
|
||||
"StringLike": {
|
||||
"AWS:PrincipalOrgID": "o-111122223333"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
is_condition_valid = False
|
||||
|
||||
# The conditions must be defined in lowercase since the context key names are not case-sensitive.
|
||||
# For example, including the aws:PrincipalOrgID context key is equivalent to testing for AWS:PrincipalOrgID
|
||||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
|
||||
valid_condition_options = {
|
||||
"StringEquals": [
|
||||
"aws:principalorgid",
|
||||
],
|
||||
"StringLike": [
|
||||
"aws:principalorgid",
|
||||
],
|
||||
}
|
||||
|
||||
for condition_operator, condition_operator_key in valid_condition_options.items():
|
||||
if condition_operator in condition_statement:
|
||||
for value in condition_operator_key:
|
||||
# We need to transform the condition_statement into lowercase
|
||||
condition_statement[condition_operator] = {
|
||||
k.lower(): v
|
||||
for k, v in condition_statement[condition_operator].items()
|
||||
}
|
||||
|
||||
if value in condition_statement[condition_operator]:
|
||||
# values are a list
|
||||
if isinstance(
|
||||
condition_statement[condition_operator][value],
|
||||
list,
|
||||
):
|
||||
is_condition_valid = True
|
||||
for item in condition_statement[condition_operator][value]:
|
||||
if item == "*":
|
||||
is_condition_valid = False
|
||||
break
|
||||
|
||||
# value is a string
|
||||
elif isinstance(
|
||||
condition_statement[condition_operator][value],
|
||||
str,
|
||||
):
|
||||
if "*" not in condition_statement[condition_operator][value]:
|
||||
is_condition_valid = True
|
||||
|
||||
return is_condition_valid
|
||||
@@ -40,7 +40,6 @@ class AWSService:
|
||||
self.audited_account_arn = provider.identity.account_arn
|
||||
self.audited_partition = provider.identity.partition
|
||||
self.audit_resources = provider.audit_resources
|
||||
# TODO: remove this
|
||||
self.audited_checks = provider.audit_metadata.expected_checks
|
||||
self.audit_config = provider.audit_config
|
||||
self.fixer_config = provider.fixer_config
|
||||
|
||||
@@ -5,7 +5,6 @@ from boto3.session import Session
|
||||
from botocore.config import Config
|
||||
|
||||
from prowler.config.config import output_file_timestamp
|
||||
from prowler.providers.aws.config import AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
from prowler.providers.aws.lib.arn.models import ARN
|
||||
from prowler.providers.common.models import ProviderOutputOptions
|
||||
|
||||
@@ -35,7 +34,7 @@ class AWSAssumeRoleInfo:
|
||||
external_id: str
|
||||
mfa_enabled: bool
|
||||
role_session_name: str
|
||||
sts_region: str = AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
sts_region: str
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -14,11 +14,11 @@ class AccessAnalyzer(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.analyzers = []
|
||||
self.__threading_call__(self._list_analyzers)
|
||||
self._list_findings()
|
||||
self._get_finding_status()
|
||||
self.__threading_call__(self.__list_analyzers__)
|
||||
self.__list_findings__()
|
||||
self.__get_finding_status__()
|
||||
|
||||
def _list_analyzers(self, regional_client):
|
||||
def __list_analyzers__(self, regional_client):
|
||||
logger.info("AccessAnalyzer - Listing Analyzers...")
|
||||
try:
|
||||
list_analyzers_paginator = regional_client.get_paginator("list_analyzers")
|
||||
@@ -57,7 +57,7 @@ class AccessAnalyzer(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_finding_status(self):
|
||||
def __get_finding_status__(self):
|
||||
logger.info("AccessAnalyzer - Get Finding status...")
|
||||
try:
|
||||
for analyzer in self.analyzers:
|
||||
@@ -87,7 +87,7 @@ class AccessAnalyzer(AWSService):
|
||||
|
||||
# TODO: We need to include ListFindingsV2
|
||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/accessanalyzer/client/list_findings_v2.html
|
||||
def _list_findings(self):
|
||||
def __list_findings__(self):
|
||||
logger.info("AccessAnalyzer - Listing Findings per Analyzer...")
|
||||
try:
|
||||
for analyzer in self.analyzers:
|
||||
|
||||
@@ -13,10 +13,10 @@ class Account(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.number_of_contacts = 4
|
||||
self.contact_base = self._get_contact_information()
|
||||
self.contacts_billing = self._get_alternate_contact("BILLING")
|
||||
self.contacts_security = self._get_alternate_contact("SECURITY")
|
||||
self.contacts_operations = self._get_alternate_contact("OPERATIONS")
|
||||
self.contact_base = self.__get_contact_information__()
|
||||
self.contacts_billing = self.__get_alternate_contact__("BILLING")
|
||||
self.contacts_security = self.__get_alternate_contact__("SECURITY")
|
||||
self.contacts_operations = self.__get_alternate_contact__("OPERATIONS")
|
||||
|
||||
if self.contact_base:
|
||||
# Set of contact phone numbers
|
||||
@@ -42,7 +42,7 @@ class Account(AWSService):
|
||||
self.contacts_operations.email,
|
||||
}
|
||||
|
||||
def _get_contact_information(self):
|
||||
def __get_contact_information__(self):
|
||||
try:
|
||||
primary_account_contact = self.client.get_contact_information()[
|
||||
"ContactInformation"
|
||||
@@ -65,7 +65,7 @@ class Account(AWSService):
|
||||
)
|
||||
return Contact(type="PRIMARY")
|
||||
|
||||
def _get_alternate_contact(self, contact_type: str):
|
||||
def __get_alternate_contact__(self, contact_type: str):
|
||||
try:
|
||||
account_contact = self.client.get_alternate_contact(
|
||||
AlternateContactType=contact_type
|
||||
|
||||
@@ -6,30 +6,29 @@ class acm_certificates_transparency_logs_enabled(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for certificate in acm_client.certificates:
|
||||
if certificate.in_use or acm_client.provider.scan_unused_services:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = certificate.region
|
||||
if certificate.type == "IMPORTED":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} is imported."
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = certificate.region
|
||||
if certificate.type == "IMPORTED":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} is imported."
|
||||
report.resource_id = certificate.id
|
||||
report.resource_details = certificate.name
|
||||
report.resource_arn = certificate.arn
|
||||
report.resource_tags = certificate.tags
|
||||
else:
|
||||
if not certificate.transparency_logging:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} has Certificate Transparency logging disabled."
|
||||
report.resource_id = certificate.id
|
||||
report.resource_details = certificate.name
|
||||
report.resource_arn = certificate.arn
|
||||
report.resource_tags = certificate.tags
|
||||
else:
|
||||
if not certificate.transparency_logging:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} has Certificate Transparency logging disabled."
|
||||
report.resource_id = certificate.id
|
||||
report.resource_details = certificate.name
|
||||
report.resource_arn = certificate.arn
|
||||
report.resource_tags = certificate.tags
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} has Certificate Transparency logging enabled."
|
||||
report.resource_id = certificate.id
|
||||
report.resource_details = certificate.name
|
||||
report.resource_arn = certificate.arn
|
||||
report.resource_tags = certificate.tags
|
||||
findings.append(report)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} has Certificate Transparency logging enabled."
|
||||
report.resource_id = certificate.id
|
||||
report.resource_details = certificate.name
|
||||
report.resource_arn = certificate.arn
|
||||
report.resource_tags = certificate.tags
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "acm_certificates_with_secure_key_algorithms",
|
||||
"CheckTitle": "Check if ACM Certificates use a secure key algorithm",
|
||||
"CheckType": [
|
||||
"Data Protection"
|
||||
],
|
||||
"ServiceName": "acm",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:acm:region:account-id:certificate/resource-id",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsCertificateManagerCertificate",
|
||||
"Description": "Check if ACM Certificates use a secure key algorithm (RSA 2048 bits or more, or ECDSA 256 bits or more). For example certificates that use RSA-1024 can be compromised because the encryption could be broken in no more than 2^80 guesses making it vulnerable to a factorization attack.",
|
||||
"Risk": "Certificates with weak RSA or ECDSA keys can be compromised because the length of the key defines the security of the encryption. The number of bits in the key determines the number of guesses an attacker would have to make in order to decrypt the data. The more bits in the key, the more secure the encryption.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/acm/latest/userguide/acm-certificate.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Ensure that all ACM certificates use a secure key algorithm. If any certificates use smaller keys, regenerate them with a secure key size and update any systems that rely on these certificates.",
|
||||
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/acm-controls.html#acm-2"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.acm.acm_client import acm_client
|
||||
|
||||
|
||||
class acm_certificates_with_secure_key_algorithms(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for certificate in acm_client.certificates:
|
||||
if certificate.in_use or acm_client.provider.scan_unused_services:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = certificate.region
|
||||
report.resource_id = certificate.id
|
||||
report.resource_details = certificate.name
|
||||
report.resource_arn = certificate.arn
|
||||
report.resource_tags = certificate.tags
|
||||
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} uses a secure key algorithm ({certificate.key_algorithm})."
|
||||
if certificate.key_algorithm in acm_client.audit_config.get(
|
||||
"insecure_algorithms", ["RSA-1024"]
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} does not use a secure key algorithm ({certificate.key_algorithm})."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -14,28 +14,17 @@ class ACM(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.certificates = []
|
||||
self.__threading_call__(self._list_certificates)
|
||||
self._describe_certificates()
|
||||
self._list_tags_for_certificate()
|
||||
self.__threading_call__(self.__list_certificates__)
|
||||
self.__describe_certificates__()
|
||||
self.__list_tags_for_certificate__()
|
||||
|
||||
def _list_certificates(self, regional_client):
|
||||
def __list_certificates__(self, regional_client):
|
||||
logger.info("ACM - Listing Certificates...")
|
||||
try:
|
||||
includes = {
|
||||
"keyTypes": [
|
||||
"RSA_1024",
|
||||
"RSA_2048",
|
||||
"RSA_3072",
|
||||
"RSA_4096",
|
||||
"EC_prime256v1",
|
||||
"EC_secp384r1",
|
||||
"EC_secp521r1",
|
||||
]
|
||||
}
|
||||
list_certificates_paginator = regional_client.get_paginator(
|
||||
"list_certificates"
|
||||
)
|
||||
for page in list_certificates_paginator.paginate(Includes=includes):
|
||||
for page in list_certificates_paginator.paginate():
|
||||
for certificate in page["CertificateSummaryList"]:
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(
|
||||
@@ -60,7 +49,6 @@ class ACM(AWSService):
|
||||
name=certificate["DomainName"],
|
||||
id=certificate["CertificateArn"].split("/")[-1],
|
||||
type=certificate["Type"],
|
||||
key_algorithm=certificate["KeyAlgorithm"],
|
||||
expiration_days=certificate_expiration_time,
|
||||
in_use=certificate.get("InUse", False),
|
||||
transparency_logging=False,
|
||||
@@ -72,7 +60,7 @@ class ACM(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_certificates(self):
|
||||
def __describe_certificates__(self):
|
||||
logger.info("ACM - Describing Certificates...")
|
||||
try:
|
||||
for certificate in self.certificates:
|
||||
@@ -90,7 +78,7 @@ class ACM(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags_for_certificate(self):
|
||||
def __list_tags_for_certificate__(self):
|
||||
logger.info("ACM - List Tags...")
|
||||
try:
|
||||
for certificate in self.certificates:
|
||||
@@ -110,7 +98,6 @@ class Certificate(BaseModel):
|
||||
name: str
|
||||
id: str
|
||||
type: str
|
||||
key_algorithm: str
|
||||
tags: Optional[list] = []
|
||||
expiration_days: int
|
||||
in_use: bool
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"SubServiceName": "rest_api",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsApiGatewayRestApi",
|
||||
"ResourceType": "AwsApiGatewayStage",
|
||||
"Description": "Check if API Gateway Stage has client certificate enabled to access your backend endpoint.",
|
||||
"Risk": "Possible man in the middle attacks and other similar risks.",
|
||||
"RelatedUrl": "",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"SubServiceName": "rest_api",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsApiGatewayRestApi",
|
||||
"ResourceType": "AwsApiGatewayStage",
|
||||
"Description": "Check if API Gateway Stage has logging enabled.",
|
||||
"Risk": "If not enabled, monitoring of service use is not possible. Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms.",
|
||||
"RelatedUrl": "",
|
||||
|
||||
@@ -15,7 +15,7 @@ class apigateway_restapi_public(Check):
|
||||
report.resource_tags = rest_api.tags
|
||||
if rest_api.public_endpoint:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} is internet accessible."
|
||||
report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} is internet accesible."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"SubServiceName": "rest_api",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsApiGatewayRestApi",
|
||||
"ResourceType": "AwsApiGatewayStage",
|
||||
"Description": "Check if API Gateway Stage has a WAF ACL attached.",
|
||||
"Risk": "Potential attacks and / or abuse of service, more even for even for internet reachable services.",
|
||||
"RelatedUrl": "",
|
||||
|
||||
@@ -14,13 +14,13 @@ class APIGateway(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.rest_apis = []
|
||||
self.__threading_call__(self._get_rest_apis)
|
||||
self._get_authorizers()
|
||||
self._get_rest_api()
|
||||
self._get_stages()
|
||||
self._get_resources()
|
||||
self.__threading_call__(self.__get_rest_apis__)
|
||||
self.__get_authorizers__()
|
||||
self.__get_rest_api__()
|
||||
self.__get_stages__()
|
||||
self.__get_resources__()
|
||||
|
||||
def _get_rest_apis(self, regional_client):
|
||||
def __get_rest_apis__(self, regional_client):
|
||||
logger.info("APIGateway - Getting Rest APIs...")
|
||||
try:
|
||||
get_rest_apis_paginator = regional_client.get_paginator("get_rest_apis")
|
||||
@@ -44,7 +44,7 @@ class APIGateway(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_authorizers(self):
|
||||
def __get_authorizers__(self):
|
||||
logger.info("APIGateway - Getting Rest APIs authorizer...")
|
||||
try:
|
||||
for rest_api in self.rest_apis:
|
||||
@@ -75,7 +75,7 @@ class APIGateway(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_rest_api(self):
|
||||
def __get_rest_api__(self):
|
||||
logger.info("APIGateway - Describing Rest API...")
|
||||
try:
|
||||
for rest_api in self.rest_apis:
|
||||
@@ -103,7 +103,7 @@ class APIGateway(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_stages(self):
|
||||
def __get_stages__(self):
|
||||
logger.info("APIGateway - Getting stages for Rest APIs...")
|
||||
try:
|
||||
for rest_api in self.rest_apis:
|
||||
@@ -151,7 +151,7 @@ class APIGateway(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_resources(self):
|
||||
def __get_resources__(self):
|
||||
logger.info("APIGateway - Getting API resources...")
|
||||
try:
|
||||
for rest_api in self.rest_apis:
|
||||
|
||||
@@ -13,11 +13,11 @@ class ApiGatewayV2(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.apis = []
|
||||
self.__threading_call__(self._get_apis)
|
||||
self._get_authorizers()
|
||||
self._get_stages()
|
||||
self.__threading_call__(self.__get_apis__)
|
||||
self.__get_authorizers__()
|
||||
self.__get_stages__()
|
||||
|
||||
def _get_apis(self, regional_client):
|
||||
def __get_apis__(self, regional_client):
|
||||
logger.info("APIGatewayv2 - Getting APIs...")
|
||||
try:
|
||||
get_apis_paginator = regional_client.get_paginator("get_apis")
|
||||
@@ -41,7 +41,7 @@ class ApiGatewayV2(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_authorizers(self):
|
||||
def __get_authorizers__(self):
|
||||
logger.info("APIGatewayv2 - Getting APIs authorizer...")
|
||||
try:
|
||||
for api in self.apis:
|
||||
@@ -54,7 +54,7 @@ class ApiGatewayV2(AWSService):
|
||||
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
|
||||
)
|
||||
|
||||
def _get_stages(self):
|
||||
def __get_stages__(self):
|
||||
logger.info("APIGatewayv2 - Getting stages for APIs...")
|
||||
try:
|
||||
for api in self.apis:
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceType": "AppStream",
|
||||
"Description": "Ensure default Internet Access from your Amazon AppStream fleet streaming instances should remain unchecked.",
|
||||
"Risk": "Default Internet Access from your fleet streaming instances should be controlled using a NAT gateway in the VPC.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceType": "AppStream",
|
||||
"Description": "Ensure user maximum session duration is no longer than 10 hours.",
|
||||
"Risk": "Having a session duration lasting longer than 10 hours should not be necessary and if running for any malicious reasons provides a greater time for usage than should be allowed.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceType": "AppStream",
|
||||
"Description": "Ensure session disconnect timeout is set to 5 minutes or less",
|
||||
"Risk": "Disconnect timeout in minutes, is the amount of of time that a streaming session remains active after users disconnect.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceType": "AppStream",
|
||||
"Description": "Ensure session idle disconnect timeout is set to 10 minutes or less.",
|
||||
"Risk": "Idle disconnect timeout in minutes is the amount of time that users can be inactive before they are disconnected from their streaming session and the Disconnect timeout in minutes time begins.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html",
|
||||
|
||||
@@ -13,10 +13,10 @@ class AppStream(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.fleets = []
|
||||
self.__threading_call__(self._describe_fleets)
|
||||
self._list_tags_for_resource()
|
||||
self.__threading_call__(self.__describe_fleets__)
|
||||
self.__list_tags_for_resource__()
|
||||
|
||||
def _describe_fleets(self, regional_client):
|
||||
def __describe_fleets__(self, regional_client):
|
||||
logger.info("AppStream - Describing Fleets...")
|
||||
try:
|
||||
describe_fleets_paginator = regional_client.get_paginator("describe_fleets")
|
||||
@@ -50,7 +50,7 @@ class AppStream(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags_for_resource(self):
|
||||
def __list_tags_for_resource__(self):
|
||||
logger.info("AppStream - List Tags...")
|
||||
try:
|
||||
for fleet in self.fleets:
|
||||
|
||||
@@ -13,12 +13,12 @@ class Athena(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.workgroups = {}
|
||||
self.__threading_call__(self._list_workgroups)
|
||||
self._get_workgroups()
|
||||
self._list_query_executions()
|
||||
self._list_tags_for_resource()
|
||||
self.__threading_call__(self.__list_workgroups__)
|
||||
self.__get_workgroups__()
|
||||
self.__list_query_executions__()
|
||||
self.__list_tags_for_resource__()
|
||||
|
||||
def _list_workgroups(self, regional_client):
|
||||
def __list_workgroups__(self, regional_client):
|
||||
logger.info("Athena - Listing WorkGroups...")
|
||||
try:
|
||||
list_workgroups = regional_client.list_work_groups()
|
||||
@@ -44,7 +44,7 @@ class Athena(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_workgroups(self):
|
||||
def __get_workgroups__(self):
|
||||
logger.info("Athena - Getting WorkGroups...")
|
||||
try:
|
||||
for workgroup in self.workgroups.values():
|
||||
@@ -88,7 +88,7 @@ class Athena(AWSService):
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_query_executions(self):
|
||||
def __list_query_executions__(self):
|
||||
logger.info("Athena - Listing Queries...")
|
||||
try:
|
||||
for workgroup in self.workgroups.values():
|
||||
@@ -109,7 +109,7 @@ class Athena(AWSService):
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags_for_resource(self):
|
||||
def __list_tags_for_resource__(self):
|
||||
logger.info("Athena - Listing Tags...")
|
||||
try:
|
||||
for workgroup in self.workgroups.values():
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:athena:region:account-id:workgroup/resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsAthenaWorkGroup",
|
||||
"ResourceType": "WorkGroup",
|
||||
"Description": "Ensure that encryption at rest is enabled for Amazon Athena query results stored in Amazon S3 in order to secure data and meet compliance requirements for data-at-rest encryption.",
|
||||
"Risk": "If not enabled sensitive information at rest is not protected.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/athena/latest/ug/encryption.html",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:athena:region:account-id:workgroup/resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsAthenaWorkGroup",
|
||||
"ResourceType": "WorkGroup",
|
||||
"Description": "Ensure that workgroup configuration is enforced so it cannot be overriden by client-side settings.",
|
||||
"Risk": "If workgroup configuration is not enforced security settings like encryption can be overriden by client-side settings.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/athena/latest/ug/workgroups-settings-override.html",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "autoscaling_find_secrets_ec2_launch_configuration",
|
||||
"CheckTitle": "[DEPRECATED] Find secrets in EC2 Auto Scaling Launch Configuration",
|
||||
"CheckTitle": "Find secrets in EC2 Auto Scaling Launch Configuration",
|
||||
"CheckType": [
|
||||
"IAM"
|
||||
],
|
||||
@@ -9,8 +9,8 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:autoscaling:region:account-id:autoScalingGroupName/resource-name",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "AwsAutoScalingLaunchConfiguration",
|
||||
"Description": "[DEPRECATED] Find secrets in EC2 Auto Scaling Launch Configuration",
|
||||
"ResourceType": "Other",
|
||||
"Description": "Find secrets in EC2 Auto Scaling Launch Configuration",
|
||||
"Risk": "The use of a hard-coded password increases the possibility of password guessing. If hard-coded passwords are used, it is possible that malicious users gain access through the account in question.",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import os
|
||||
import tempfile
|
||||
import zlib
|
||||
from base64 import b64decode
|
||||
|
||||
from detect_secrets import SecretsCollection
|
||||
from detect_secrets.settings import default_settings
|
||||
|
||||
from prowler.config.config import encoding_format_utf_8
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import detect_secrets_scan
|
||||
from prowler.providers.aws.services.autoscaling.autoscaling_client import (
|
||||
autoscaling_client,
|
||||
)
|
||||
@@ -13,9 +17,6 @@ from prowler.providers.aws.services.autoscaling.autoscaling_client import (
|
||||
class autoscaling_find_secrets_ec2_launch_configuration(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
secrets_ignore_patterns = autoscaling_client.audit_config.get(
|
||||
"secrets_ignore_patterns", []
|
||||
)
|
||||
for configuration in autoscaling_client.launch_configurations:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = configuration.region
|
||||
@@ -23,7 +24,9 @@ class autoscaling_find_secrets_ec2_launch_configuration(Check):
|
||||
report.resource_arn = configuration.arn
|
||||
|
||||
if configuration.user_data:
|
||||
temp_user_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
user_data = b64decode(configuration.user_data)
|
||||
|
||||
try:
|
||||
if user_data[0:2] == b"\x1f\x8b": # GZIP magic number
|
||||
user_data = zlib.decompress(
|
||||
@@ -42,16 +45,22 @@ class autoscaling_find_secrets_ec2_launch_configuration(Check):
|
||||
)
|
||||
continue
|
||||
|
||||
has_secrets = detect_secrets_scan(
|
||||
data=user_data, excluded_secrets=secrets_ignore_patterns
|
||||
temp_user_data_file.write(
|
||||
bytes(user_data, encoding="raw_unicode_escape")
|
||||
)
|
||||
temp_user_data_file.close()
|
||||
secrets = SecretsCollection()
|
||||
with default_settings():
|
||||
secrets.scan_file(temp_user_data_file.name)
|
||||
|
||||
if has_secrets:
|
||||
if secrets.json():
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Potential secret found in autoscaling {configuration.name} User Data."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"No secrets found in autoscaling {configuration.name} User Data."
|
||||
|
||||
os.remove(temp_user_data_file.name)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"No secrets found in autoscaling {configuration.name} since User Data is empty."
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:autoscaling:region:account-id:autoScalingGroupName/resource-name",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsAutoScalingAutoScalingGroup",
|
||||
"ResourceType": "Other",
|
||||
"Description": "EC2 Auto Scaling Group should use multiple Availability Zones",
|
||||
"Risk": "In case of a failure in a single Availability Zone, the Auto Scaling Group will not be able to launch new instances to replace the failed ones.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-add-availability-zone.html",
|
||||
|
||||
@@ -11,11 +11,11 @@ class AutoScaling(AWSService):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.launch_configurations = []
|
||||
self.__threading_call__(self._describe_launch_configurations)
|
||||
self.__threading_call__(self.__describe_launch_configurations__)
|
||||
self.groups = []
|
||||
self.__threading_call__(self._describe_auto_scaling_groups)
|
||||
self.__threading_call__(self.__describe_auto_scaling_groups__)
|
||||
|
||||
def _describe_launch_configurations(self, regional_client):
|
||||
def __describe_launch_configurations__(self, regional_client):
|
||||
logger.info("AutoScaling - Describing Launch Configurations...")
|
||||
try:
|
||||
describe_launch_configurations_paginator = regional_client.get_paginator(
|
||||
@@ -44,7 +44,7 @@ class AutoScaling(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_auto_scaling_groups(self, regional_client):
|
||||
def __describe_auto_scaling_groups__(self, regional_client):
|
||||
logger.info("AutoScaling - Describing AutoScaling Groups...")
|
||||
try:
|
||||
describe_auto_scaling_groups_paginator = regional_client.get_paginator(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user