Analyzing ScoutSuite
Source: nccgroup/ScoutSuite @
7909f2f
How It Works
ScoutSuite is a read-only multi-cloud security posture assessment tool. It authenticates to a cloud provider (AWS, Azure, GCP, Alibaba Cloud, Oracle, DigitalOcean, or Kubernetes), enumerates resource configurations via provider APIs, then evaluates those configurations against a library of JSON-encoded security rules. The output is a self-contained HTML report that opens locally in a browser—no server infrastructure required.
The system operates in a clear pipeline: authenticate → fetch → preprocess → analyze → postprocess → report. During the fetch phase, each provider’s services are gathered concurrently using Python asyncio. After fetching, a preprocessing step enriches the data with cross-service correlations (e.g., matching IAM roles to EC2 instances). The rule engine then walks the collected configuration tree, evaluating declarative conditions expressed as JSON path expressions and comparison operators. Findings are collected, optionally filtered through a user-supplied exceptions file, and rendered into a static HTML dashboard with drill-down capabilities.
Critically, ScoutSuite never modifies cloud resources. It is purely an inspection tool—making it safe to run against production environments without risk of side effects.
Architecture
The codebase is organized around a provider-plugin architecture with a shared core:
| Layer | Location | Responsibility |
|---|---|---|
| Core engine | ScoutSuite/core/ | CLI parsing, rule processing engine, condition evaluation, exceptions |
| Providers | ScoutSuite/providers/{aws,azure,gcp,...}/ | Per-cloud authentication, API facades, resource fetchers |
| Rules | ScoutSuite/providers/{cloud}/rules/ | JSON findings, filters, reusable conditions |
| Output | ScoutSuite/output/ | HTML report generation, JSON/SQLite encoding |
Each provider follows an identical structure:
providers/<cloud>/
├── authentication_strategy.py # Credential acquisition
├── provider.py # Provider class (extends BaseProvider)
├── facade/ # Thin API wrappers (boto3, azure sdk, etc.)
│ └── base.py # Session/client management
├── resources/ # Resource fetchers (extend base Resources class)
│ └── <service>/base.py # Hierarchical resource tree builder
├── services.py # ServicesConfig — wires facades to resources
├── rules/
│ ├── findings/ # Individual security rules (JSON)
│ ├── filters/ # Display filter rules (JSON)
│ ├── conditions/ # Reusable condition snippets
│ └── rulesets/default.json # Which rules to enable + severity levels
└── metadata.json # UI metadata for report rendering
The resource fetching uses a recursive CompositeResources pattern. Each resource type is a dict subclass with a fetch_all() coroutine. CompositeResources declare _children — nested resource types — and fetch them concurrently via asyncio.wait(). This creates a tree: EC2 → Regions → VPCs → SecurityGroups → Rules → Protocols → Ports.
After fetching, the object tree is serialized to a plain dict (report.encoder.to_dict(self.services)) because the legacy rule engine and HTML report expect dictionary structures, not objects.
The Spine
A complete scan flows through these stages in __main__.py:
-
run_from_cli()— Parses CLI arguments, delegates torun()which sets up anasyncioevent loop with aThrottlerfor API rate limiting and aThreadPoolExecutor. -
_run()— The async orchestrator. In sequence:- Authenticate —
get_authentication_strategy(provider)returns a provider-specific strategy;authenticate()yields a credentials object. - Create provider —
get_provider()instantiates the provider class (e.g.,AWSProvider), which builds aServicesConfigwith facades and resource fetchers. - Fetch —
await cloud_provider.fetch()triggers concurrentfetch_all()for each service, recursively gathering all resources. - Preprocess —
cloud_provider.preprocessing()enriches cross-service data (e.g., correlating IAM policies with S3 buckets, mapping security groups to ELBs). - Analyze (findings) —
Rulesetloads the default ruleset JSON;ProcessingEngine.run()walks the config tree, evaluating each rule’s conditions viacore/utils.py:recurse()andcore/conditions.py:pass_conditions(). - Analyze (filters) — A second pass with
filters.jsonapplies display filters using the same engine. - Exceptions —
RuleExceptions.process()removes user-exempted findings. - Postprocess — Updates metadata counts, summary statistics, last-run info.
- Report —
ScoutReport.save()serializes results to JSON, copies static HTML/CSS/JS assets, and generates a self-contained HTML file.
- Authenticate —
-
Exit codes follow a convention:
0= success,101= auth failure,104= fetch failure,106= rule engine failure,200= success with handled errors.
Key Patterns
Declarative rule definitions. Rules are JSON files containing a dot-path (cloudtrail.regions.id.trails.id), a set of conditions with operators (equal, notEqual, empty, containString, isCrossAccount, olderThan, etc.), and metadata (description, rationale, remediation, CIS compliance references). The condition DSL is remarkably expressive—supporting and/or logic, dynamic value resolution via _GET_VALUE_AT_(), IP range injection via _IP_RANGES_FROM_FILE_(), and reusable condition includes via _INCLUDE_().
Provider as a dictionary. After fetching, the provider’s service tree is a deeply nested dict. This is both a strength (the rule engine can traverse any path generically) and a legacy constraint (all rule evaluation operates on raw dicts rather than typed objects).
Facade pattern for API isolation. Each cloud service has a facade/<service>.py that wraps SDK calls with error handling and optional region iteration. Resources depend on facades, not on SDK clients directly, making the resource layer testable.
Proprietary service plugin system. AWS services like Cognito, ECR, ECS, EKS, GuardDuty, and SSM are imported with try/except NameError blocks, suggesting an optional proprietary add-on model layered on top of the open-source core.
Metadata-driven callbacks. The metadata.json files define resource paths and callback functions. During preprocessing, _process_metadata_callbacks() walks the metadata and invokes named callbacks on the provider instance — a form of declarative cross-service data enrichment.
Non-Obvious Details
The rule engine operates on serialized dicts, not live objects
After
fetch()completes,BaseProvider.fetch()callsreport.encoder.to_dict(self.services)to flatten theResourcesobject tree back into plain dictionaries. This means the richResourcessubclasses with their facades are discarded before analysis. The rule engine (ProcessingEngine) only ever seesdictdata.
_STRIPDOTS_and_ARG_are inline template macrosRule definitions use string substitution patterns like
_ARG_0_for parameterized arguments and_STRIPDOTS_(value)to remove dots from strings. These are resolved duringRule.set_definition()via regex replacement on the raw JSON string before parsing. It’s a preprocessor embedded in the rule loader.
policyuniverseis a key dependencyThe condition evaluator uses
policyuniversefor AWS IAM policy analysis — expanding wildcards in action statements and testing policy containment. This enables rules likecontainActionandnotContainActionwithout re-implementing ARN matching.
Two nearly identical recursive traversal methods
BaseProviderhas both_go_to_and_do()and_new_go_to_and_do(). The difference is that_new_go_to_and_dosupports multiple callbacks per target and resolves callback names as method attributes. The older_go_to_and_dois still used inAWSProvider.preprocessing(). This appears to be an incomplete refactor.
Python 3.10 compatibility hack
The
__main__.pyincludescollections.Callable = collections.abc.Callable— a workaround forasyncio_throttlerelying on the removedcollections.Callablealias.
Rule versioning via
versionskeyA single rule file can define multiple variants through the
versionskey in the ruleset, each with different arguments and akey_suffix. This allows one rule definition to generate multiple findings (e.g., checking multiple port ranges from the same template).
Assessment
Strengths:
- Remarkably extensible rule system. Adding a new security check is often just creating a JSON file — no Python required. The condition DSL covers most common scenarios (equality, regex, CIDR matching, date comparison, IAM policy analysis). 578 total findings across all providers (147 for AWS alone) is substantial coverage.
- Clean provider abstraction. Adding a new cloud provider requires implementing authentication, a facade, resource fetchers, and rules — all following well-defined patterns. The codebase supports 7 providers (AWS, Azure, GCP, Alibaba, OCI, DigitalOcean, Kubernetes).
- Safe by design. Read-only operation means zero risk to production environments.
- Self-contained HTML output. The report is a single-page app with all data embedded, requiring no server or network access to review.
Concerns:
- The dict-everywhere pattern is fragile. The rule engine traverses raw dictionaries with string paths. There’s no schema validation on fetched data, so API changes or missing keys cause cryptic failures deep in
recurse(). - Incomplete refactor. The coexistence of
_go_to_and_doand_new_go_to_and_do, thedictserialization after fetch, and theto_dict()workaround all suggest an architectural migration that stalled partway through. - Tight coupling between AWSProvider and preprocessing. The
AWSProvider.preprocessing()method is ~500 lines of cross-service correlation logic that would benefit from being data-driven (like the metadata callback system) rather than hand-coded. - No unit tests for rules. The
tests/directory covers core mechanics but individual finding rules are not automatically tested. A rule with a malformed path or condition will silently fail at runtime.
Recommendations:
- Validate fetched resource structures against schemas before rule evaluation.
- Complete the
_new_go_to_and_domigration and remove the legacy traversal method. - Add automated testing for rule definitions — at minimum, a lint pass that verifies all paths referenced in rules exist in the metadata.
- Consider typing the resource tree (at minimum with
TypedDict) to enable static analysis.
Related
- analyzing-nuclei
- analyzing-pacu
- analyzing-bloodhound
- analyzing-spiderfoot