Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inventory): add prowler scanner-inventory(auditor mode) #5265

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/tutorials/scan-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Scan Inventory

The scan-inventory feature is a tool that generates a JSON report within the `/output/inventory/<provider>` directory and the scanned service. This feature allows you to perform a inventory of the resources existing in your provider that are scanned by Prowler.

## Usage

To use the scan-inventory feature, run Prowler with the `--scan-inventory` option. For example:

```
prowler <provider> --scan-inventory
```

This will generate a JSON report within the `/output/inventory/<provider>` directory and the scanned service.

## Output Directory Contents

The contents of the `/output/<provider>` directory and the scanned service depend on the Prowler execution. This directory contains all the information gathered during scanning, including a JSON report containing all the gathered information.

## Limitations

The scan-inventory feature has some limitations. For example:

* It is only available for the AWS provider.
* It only contains the information retrieved by Prowler during the execution.

## Example

Here's an example of how to use the scan-inventory feature and the contents of the `/output/inventory/<provider>` directory and the scanned service:

`prowler aws -s ec2 --scan-inventory`

```
/output/inventory/aws directory
|
|-- ec2
| |
| |-- ec2_output.json
```
In this example, Prowler is run with the `-s ec2` and `--scan-inventory` options for the AWS provider. The `/output/inventory/aws` directory contains a JSON report showing all the information gathered during scanning.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ nav:
- Dashboard: tutorials/dashboard.md
- Fixer (remediations): tutorials/fixer.md
- Quick Inventory: tutorials/quick-inventory.md
- Scan Inventory: tutorials/scan-inventory.md
- Slack Integration: tutorials/integrations.md
- Configuration File: tutorials/configuration_file.md
- Logging: tutorials/logging.md
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions prowler/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
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.common.scan_inventory import run_prowler_scan_inventory

Check warning on line 76 in prowler/__main__.py

View check run for this annotation

Codecov / codecov/patch

prowler/__main__.py#L76

Added line #L76 was not covered by tests
from prowler.providers.gcp.models import GCPOutputOptions
from prowler.providers.kubernetes.models import KubernetesOutputOptions

Expand Down Expand Up @@ -688,6 +689,11 @@
if checks_folder:
remove_custom_checks_module(checks_folder, provider)

# Run the quick inventory for the provider if available
if hasattr(args, "scan_inventory") and args.scan_inventory:
run_prowler_scan_inventory(checks_to_execute, args.provider)
sys.exit()

Check warning on line 695 in prowler/__main__.py

View check run for this annotation

Codecov / codecov/patch

prowler/__main__.py#L693-L695

Added lines #L693 - L695 were not covered by tests

# If there are failed findings exit code 3, except if -z is input
if (
not args.ignore_exit_code_3
Expand Down
7 changes: 7 additions & 0 deletions prowler/providers/aws/lib/arguments/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ def init_parser(self):
action="store_true",
help="Run Prowler Quick Inventory. The inventory will be stored in an output csv by default",
)
# AWS Scan Inventory
aws_scan_inventory_subparser = aws_parser.add_argument_group("Scan Inventory")
aws_scan_inventory_subparser.add_argument(
"--scan-inventory",
action="store_true",
help="Run Prowler Scan Inventory. The inventory will be stored in an output json file.",
)
# AWS Outputs
aws_outputs_subparser = aws_parser.add_argument_group("AWS Outputs to S3")
aws_outputs_bucket_parser = aws_outputs_subparser.add_mutually_exclusive_group()
Expand Down
47 changes: 47 additions & 0 deletions prowler/providers/aws/lib/service/service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from collections import deque
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from typing import Any, Dict

from prowler.lib.logger import logger
from prowler.providers.aws.aws_provider import AwsProvider
Expand Down Expand Up @@ -101,3 +104,47 @@
except Exception:
# Handle exceptions if necessary
pass # Replace 'pass' with any additional exception handling logic. Currently handled within the called function

def __to_dict__(self, seen=None) -> Dict[str, Any]:
if seen is None:
seen = set()

Check warning on line 110 in prowler/providers/aws/lib/service/service.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/lib/service/service.py#L109-L110

Added lines #L109 - L110 were not covered by tests

def convert_value(value):
if isinstance(value, (AwsProvider,)):
return {}
if isinstance(value, datetime):
return value.isoformat() # Convert datetime to ISO 8601 string
elif isinstance(value, deque):
return [convert_value(item) for item in value]
elif isinstance(value, list):
return [convert_value(item) for item in value]
elif isinstance(value, tuple):
return tuple(convert_value(item) for item in value)
elif isinstance(value, dict):

Check warning on line 123 in prowler/providers/aws/lib/service/service.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/lib/service/service.py#L112-L123

Added lines #L112 - L123 were not covered by tests
# Ensure keys are strings and values are processed
return {

Check warning on line 125 in prowler/providers/aws/lib/service/service.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/lib/service/service.py#L125

Added line #L125 was not covered by tests
convert_value(str(k)): convert_value(v) for k, v in value.items()
}
elif hasattr(value, "__dict__"):
obj_id = id(value)
if obj_id in seen:
return None # Avoid infinite recursion
seen.add(obj_id)
return {key: convert_value(val) for key, val in value.__dict__.items()}

Check warning on line 133 in prowler/providers/aws/lib/service/service.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/lib/service/service.py#L128-L133

Added lines #L128 - L133 were not covered by tests
else:
return value # Handle basic types and non-serializable objects

Check warning on line 135 in prowler/providers/aws/lib/service/service.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/lib/service/service.py#L135

Added line #L135 was not covered by tests

return {

Check warning on line 137 in prowler/providers/aws/lib/service/service.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/lib/service/service.py#L137

Added line #L137 was not covered by tests
key: convert_value(value)
for key, value in self.__dict__.items()
if key
not in [
"audit_config",
"provider",
"session",
"regional_clients",
"client",
"thread_pool",
"fixer_config",
]
}
104 changes: 104 additions & 0 deletions prowler/providers/common/scan_inventory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import importlib
import json
import os
from collections import deque
from datetime import datetime

from colorama import Fore, Style
from pydantic import BaseModel

from prowler.config.config import orange_color


def run_prowler_scan_inventory(checks_to_execute, provider):
output_folder_path = f"./output/inventory/{provider}"

os.makedirs(output_folder_path, exist_ok=True)

# Recursive function to handle serialization
def class_to_dict(obj, seen=None):
if seen is None:
seen = set()

if isinstance(obj, dict):
new_dict = {}
for key, value in obj.items():
if isinstance(key, tuple):
key = str(key) # Convert tuple to string
new_dict[key] = class_to_dict(value)
return new_dict
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, deque):
return list(class_to_dict(item, seen) for item in obj)
elif isinstance(obj, BaseModel):
return obj.dict()
elif isinstance(obj, (list, tuple)):
return [class_to_dict(item, seen) for item in obj]
elif hasattr(obj, "__dict__") and id(obj) not in seen:
seen.add(id(obj))
return {
key: class_to_dict(value, seen) for key, value in obj.__dict__.items()
}
else:
return obj

service_set = set()

for check_name in checks_to_execute:
try:
service = check_name.split("_")[0]

if service in service_set:
continue

service_set.add(service)

service_path = f"./prowler/providers/{provider}/services/{service}"

# List to store all _client filenames
client_files = []

# Walk through the directory and find all files
for root, dirs, files in os.walk(service_path):
for file in files:
if file.endswith("_client.py"):
# Append only the filename to the list (not the full path)
client_files.append(file)

service_output_folder = f"{output_folder_path}/{service}"

os.makedirs(service_output_folder, exist_ok=True)

for service_client in client_files:

service_client = service_client.split(".py")[0]
check_module_path = (
f"prowler.providers.{provider}.services.{service}.{service_client}"
)

try:
lib = importlib.import_module(f"{check_module_path}")
except ModuleNotFoundError:
print(f"Module not found: {check_module_path}")
break
except Exception as e:
print(f"Error while importing module {check_module_path}: {e}")
break

client_path = getattr(lib, f"{service_client}")

# Convert to JSON
output_file = service_client.split("_client")[0]

with open(
f"{service_output_folder}/{output_file}_output.json", "w+"
) as fp:
output = client_path.__to_dict__()
json.dump(output, fp=fp, default=str, indent=4)

except Exception as e:
print("Exception: ", e)
print(
f"\n{Style.BRIGHT}{Fore.GREEN}Scan inventory for {provider} results: {orange_color}{output_folder_path}"
)
6 changes: 6 additions & 0 deletions tests/lib/cli/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,12 @@ def test_aws_parser_quick_inventory_long(self):
parsed = self.parser.parse(command)
assert parsed.quick_inventory

def test_aws_parser_scan_inventory_long(self):
argument = "--scan-inventory"
command = [prowler_command, argument]
parsed = self.parser.parse(command)
assert parsed.scan_inventory

def test_aws_parser_output_bucket_short(self):
argument = "-B"
bucket = "test-bucket"
Expand Down