Compare commits

..
484 Commits
Author SHA1 Message Date
jbarciabf f65be085cd Merge pull request #138 from BishopFox/azure_improvements
Azure improvements and new modules
2026-11-02 08:22:04 -05:00
Bastien Faure 16320591ea Migrated parts of whoami into tokencache, stuffed existing whoami with entra ID discovery 2026-10-30 21:28:15 -04:00
Bastien Faure 5267f430b0 fix 2026-10-30 20:39:48 -04:00
Bastien Faure 4a63b18fd3 Updated automation in azure, use shared logger 2026-10-30 20:35:07 -04:00
Bastien Faure 0642b82438 Updated functions, use new logger 2026-10-30 20:34:38 -04:00
Bastien Faure e688d79ed2 Migrated azure commands to shared logger 2026-10-30 17:37:16 -04:00
Bastien Faure 2f0cfd7b1f Added Azure automation command 2026-10-29 23:55:08 -04:00
Bastien Faure e3a1803cce Added Azure Func module 2026-10-29 23:54:16 -04:00
Bastien Faure 6257da9391 Added ACR module for Azure 2026-10-29 23:51:47 -04:00
Bastien Faure d44899f213 Merge pull request #134 from sethsec/fix-region-slowness
Fix region slowness and hangs with opt-in/unreachable regions
2026-08-20 14:39:52 -04:00
Seth Art 29ab782b31 Fix region slowness and hangs with opt-in/unreachable regions
Commands were hanging for several minutes when run with low-privilege
credentials against accounts where opt-in regions (e.g. me-south-1)
are not enabled. Each unreachable region exhausted the full AWS SDK
retry budget (3 attempts × ~30s timeout) per service call, compounding
across every check in all-checks mode.

Three-part fix:

1. TCP reachability probe: Before making any API calls, probe
   sts.REGION.amazonaws.com:443 with a 3-second timeout. Unreachable
   regions are filtered out concurrently and cached for the run so the
   probe only happens once per region. This eliminates multi-minute
   stalls caused by regions that are not enabled for the account.

2. --max-retries flag: New persistent flag on the aws subcommand
   (default 3, matching prior behaviour). Setting --max-retries 0
   skips all retries, useful when running black-box against an account
   where most API calls will return 403/401 anyway.

3. Remove me-south-1 from the static fallback region list: This
   Bahrain opt-in region was the primary offender reported in issue
   #132. It is excluded from the hardcoded list so accounts that never
   opted in skip it entirely even before the TCP probe runs.

Also fixes a pre-existing bug in GetIamSimResult where a redundant
second isRoleAdminBool check after the cache-or-compute block would
overwrite a correct "YES" result from the cache with "No" (because
isRoleAdminBool stays false on a cache hit).

Fixes #132
2026-06-17 14:29:10 -04:00
jbarciabf ba4ff4701a Merge pull request #130 from BishopFox/fix/network-ports-map-race
Fix concurrent map access race in network-ports module and bump version to 2.0.5
2026-05-26 10:45:11 -04:00
jbarciabf 81be56e006 Bump version to 2.0.5 2026-05-26 10:42:29 -04:00
jbarciabf 494443d7ce Fix concurrent map access race in network-ports module (#129)
The NetworkPortsModule cached EC2 NACLs and Security Groups in two
per-region maps. The cache lookup read the maps without holding a
lock while the cache population path took a write lock, producing a
"concurrent map read and map write" runtime panic when multiple
region goroutines hit the cache check concurrently. The panic
reproduced reliably in low-density AWS accounts where every region
goroutine raced through the cache check at the same instant.

Take the read lock on the cache fast-path in both
getEC2NACLsPerRegion and getEC2SecurityGroupsPerRegion. Move
naclsMutex and securityGroupsMutex from package-level vars onto the
module struct so concurrent module instances stop serializing on
each other's locks.

Convert CommandCounter from plain int fields with naked ++/-- to
int64 fields with Incr/Decr/Load methods that use atomic.AddInt64
and atomic.LoadInt64 internally. Worker goroutines and the spinner
goroutine were updating and reading the same counters without
synchronization, producing torn integer writes that could show as
off-by-one progress in the spinner. Plain int64 storage keeps the
struct copyable so existing constructors and helpers that take
modules by value continue to vet-clean.

Add TestNetworkPortsCacheConcurrentAccess as a regression test.
With the read lock removed, "go test -race" reports the
mapaccess2_faststr read racing the locked write; with the fix in
place the test passes.
2026-05-26 10:36:17 -04:00
Joseph Barcia 3d3cfc6c0c Update GitHub Actions to Node 24 compatible versions
- actions/checkout v4 -> v6
- actions/setup-go v4 -> v5
- go-version ^1.20 -> ^1.25
- Replace abandoned marvinpinto/action-automatic-releases with
  softprops/action-gh-release v2

Node.js 20 actions are deprecated and will be forced to Node 24
starting June 2, 2026, and removed September 16, 2026.
2026-05-26 10:30:22 -04:00
jbarciabf 1b0925877d Merge pull request #127 from BishopFox/fix/pmapper-basepath-all-checks
Fix pmapper-data-basepath ignored in all-checks and instances race condition
2026-04-21 09:06:40 -04:00
Joseph Barcia 7727d1d08c Fix pmapper-data-basepath ignored in all-checks and race condition in instances module
The pmapperCommand in runAllChecksCommand was missing the
PmapperDataBasePath parameter, causing --pmapper-data-basepath to be
ignored for the pmapper module when running all-checks. (Fixes #105)

The instances module had a race condition where role analysis ran before
the receiver goroutine finished collecting all MappedInstances. This
caused IsAdminRole and CanPrivEscToAdmin columns to appear empty in
all-checks despite pmapper data being available. Fixed by signaling
the receiver to stop and waiting for it to drain before running role
analysis.

Version bumped to 2.0.4.
2026-04-21 13:06:11 +00:00
jbarciabf c7f81dc5cc Merge pull request #126 from mitre88/main
Fix --pmapper-data-basepath ignored in all-checks for EC2 user data module
2026-04-21 09:02:34 -04:00
Open Source Contributor 008fb84d23 Fix --pmapper-data-basepath ignored in all-checks for EC2 user data module
The EC2 user data InstancesModule in runAllChecksCommand was not receiving
the PmapperDataBasePath parameter, causing the --pmapper-data-basepath flag
to be ignored when running all-checks.

Fix: Add PmapperDataBasePath: PmapperDataBasePath to the ec2UserData struct
initialization in runAllChecksCommand.
2026-04-20 20:38:56 -06:00
jbarciabf 142757b5c2 Merge pull request #125 from BishopFox/fix/go-stdlib-vulnerabilities
Upgrade Go to 1.25.9, fix 24 stdlib vulnerabilities
2026-04-13 09:06:34 -04:00
Joseph Barcia 146034d619 Update AWS SDK to fix EventStream decoder panic (GHSA-xmrv-pmrh-hhx2)
Updates aws-sdk-go-v2/aws/protocol/eventstream to v1.7.8,
service/s3 to v1.97.3, service/lambda to v1.88.5, and
service/kinesis to v1.43.5.
2026-04-13 12:59:37 +00:00
Joseph Barcia 9e056b29d7 Update opentelemetry-go to v1.43.0 and go-jose to v4.1.4
Fixes CVE-2026-39883 (PATH hijacking in otel SDK) and
CVE-2026-34986 (panic in JWE decryption in go-jose).
2026-04-13 12:55:22 +00:00
Joseph Barcia 871491738b Upgrade Go to 1.25.9 and bump version to 2.0.3
Fixes 24 standard library vulnerabilities in crypto/tls, crypto/x509,
net/url, net/http, html/template, encoding/asn1, encoding/pem,
net/mail, net/textproto, os, and os/exec.
2026-04-12 18:16:58 +00:00
jbarciabf 033c953d57 Merge pull request #124 from BishopFox/version-bump-2.0.2
Bump version to 2.0.2
2026-04-12 13:49:44 -04:00
Joseph Barcia a7e45b9ae5 Bump version to 2.0.2 2026-04-12 17:47:40 +00:00
jbarciabf ce513ba04d Merge pull request #123 from sethsec/fix/awsservicemap-v1.1.0-service-slugs
Fix service slug mismatches for awsservicemap v1.1.0
2026-04-12 13:46:14 -04:00
Joseph Barcia 83f9af249d Update google.golang.org/grpc to v1.79.3 to fix GO-2026-4762
Fixes authorization bypass via missing leading slash in :path header.
2026-04-12 17:40:35 +00:00
Seth Art 11652a9a6e Fix service slug mismatches for awsservicemap v1.1.0 across all commands
awsservicemap v1.1.0 switched to URL-based service slugs, breaking all
commands that used the old identifiers. Affected slugs and their fixes:

  secretsmanager  -> secrets-manager   (resource-trusts, inventory)
  ssm             -> systems-manager   (inventory)
  elb             -> elasticloadbalancing (endpoints, inventory, network-ports)
  es              -> opensearch-service   (endpoints, inventory, resource-trusts)
  mq              -> amazon-mq            (endpoints, inventory)
  stepfunctions   -> step-functions       (inventory)
  sagemaker       -> sagemaker-ai         (env-vars)
  clouddirectory  -> directoryservice     (directory-services)
  kinesis         -> streams              (inventory)
  lightsail       -> amazonlightsail.com  (endpoints, env-vars, network-ports)

Each mismatch caused IsServiceInRegion to return false for all regions,
silently skipping enumeration of that service entirely.
2026-04-11 11:40:48 -04:00
Seth Art 6ac7a0bf6b Fix secrets command skipping all regions due to stale service slug names
awsservicemap v1.1.0 changed to a URL-based service identification format.
The secrets module was passing old-style slugs ("secretsmanager", "ssm") to
IsServiceInRegion, which never matched the new URL-derived slugs
("secrets-manager", "systems-manager"). This caused 0 tasks to be queued
and no secrets to be returned despite secrets existing in the account.
2026-04-11 11:40:48 -04:00
jbarciabf c6fcdba665 Merge pull request #122 from jbarciabf/fix/CVE-2026-24051
v2.0.1 - CVE-2026-24051 Fix
2026-03-09 10:10:46 -04:00
jbarciabf 4f2c2cb4c9 Bump go.opentelemetry.io/otel/sdk to v1.40.0 (CVE-2026-24051)
Fixes PATH hijacking vulnerability in the OTel Go SDK on macOS/Darwin.
2026-03-05 08:17:36 -05:00
jbarciabf db8a6895c3 Merge pull request #118 from prdngr/fix-privesc-false-positive
Fix false positive where GitHub Actions subject was not recognized
2026-03-04 09:18:59 -05:00
Dominik Prodinger 11763f04f6 Merge branch 'BishopFox:main' into fix-privesc-false-positive 2026-03-04 10:39:12 +01:00
Bastien Faure 0ba9e2d37f Merge pull request #121 from BishopFox/jb/gcp-dev
v2.0.0 - GCP Rewrite
2026-02-27 09:05:41 -05:00
jbarciabf a3d1c09558 fixed codespell referer error 2026-02-26 16:32:53 -05:00
jbarciabf a3b7ab4269 rename buckets -> storage 2026-02-26 15:01:18 -05:00
jbarciabf 60c6b8d61c added log-enum, bigquery-enum, bigtable-enum, spanner-enum to search for sensitive stored data 2026-02-26 12:55:50 -05:00
jbarciabf 8db014b136 updated foxmapper mappings 2026-02-21 14:12:23 -05:00
jbarciabf 72cd367086 fixed potential panics 2026-02-20 16:12:28 -05:00
jbarciabf f2d4b7c535 updated readme 2026-02-20 13:06:07 -05:00
jbarciabf 47d69d870f remove alias due to conflict 2026-02-20 12:59:43 -05:00
jbarciabf c9d1c2de68 normalized loot files and added additional commands 2026-02-20 11:59:20 -05:00
jbarciabf a8e802404f code cleanup 2026-02-20 08:31:44 -05:00
jbarciabf d021ea54de cleanup output, better streaming and file splitting 2026-02-19 18:17:24 -05:00
jbarciabf 7a18090609 removed attackpath and orgcache flags and enable by default 2026-02-16 15:17:04 -05:00
jbarciabf cb8be678e7 removed attack paths to use foxmapper instead 2026-02-15 23:02:27 -05:00
Dominik Prodinger 6f45cbe900 Fix false positive where GitHub Actions subject was not recognized 2026-02-09 12:19:05 +01:00
jbarciabf 8a248e3127 updated instances 2026-02-04 21:53:51 -05:00
jbarciabf de141300e9 fixed enumerate regions where permissions were sometimes denied 2026-02-04 20:35:44 -05:00
jbarciabf eb76d76dea physical cache instead of memory. 1st wave of cleanup 2026-02-04 14:15:16 -05:00
jbarciabf 72a551b38b centralized attackpath service and playbooks 2026-01-30 10:53:15 -05:00
jbarciabf 59bac4ee36 fixed whoami output and added new attack paths 2026-01-30 09:22:38 -05:00
jbarciabf 44bf8e8915 added attackpaths and loot 2026-01-29 16:16:17 -05:00
jbarciabf 70107c607f updated session handling 2026-01-23 10:52:42 -05:00
jbarciabf d24f817bf9 changed global all projects flag 2026-01-20 14:33:22 -05:00
jbarciabf 711e29e3bb updated readme permissions 2026-01-20 08:33:20 -05:00
jbarciabf 04bb167273 updated version number 2026-01-20 08:20:09 -05:00
jbarciabf ed732d0c01 migrated over to sdk and caching for standardization with aws modules 2026-01-19 15:35:39 -05:00
jbarciabf f82547b438 updated attackpath output 2026-01-16 11:36:25 -05:00
jbarciabf 35fca31475 updated whoami counts 2026-01-16 10:02:10 -05:00
jbarciabf 888649616f added attackpath flag 2026-01-16 00:32:45 -05:00
jbarciabf 2b272a1335 merged privesc, lat movemet, and data exfil into attack paths 2026-01-16 00:21:08 -05:00
jbarciabf c94996e470 added network diagrams 2026-01-15 21:39:46 -05:00
jbarciabf 2f80731e5b updated bucketenum to no limits and enumerate all 2026-01-15 20:36:11 -05:00
jbarciabf d473e31c06 updated privesc checks 2026-01-15 17:41:31 -05:00
jbarciabf baf62fdbb2 updated whoami for better privesc description 2026-01-15 14:38:59 -05:00
jbarciabf f3f98f24e3 add org tree 2026-01-15 13:51:19 -05:00
jbarciabf eedf397c93 created beautified ASCII tree view of the organization hierarchy 2026-01-15 08:02:49 -05:00
jbarciabf 31e2fe5fcb fixed privesc execution and output issues 2026-01-14 15:25:47 -05:00
jbarciabf 06a9dabf48 refactored output to be hierarchical, added privesc checks 2026-01-14 14:51:54 -05:00
jbarciabf ce545ca9ff updated dataexfil 2026-01-13 16:23:08 -05:00
jbarciabf 8954fc2a72 fixed permissions denied error 2026-01-13 15:37:53 -05:00
jbarciabf 7bb7d947ef fixed logging newline, more 400 errors, text formatting, and enhancements in dns, filestore, endpoints, and dataefil 2026-01-13 15:22:36 -05:00
jbarciabf 3c1213b0ff fixed cloudrun 400 error 2026-01-13 11:03:22 -05:00
jbarciabf fbc862aef1 updated readme 2026-01-12 21:38:31 -05:00
jbarciabf 8dddcc83d2 Merge remote-tracking branch 'upstream/main' into cloudfox-gcp-new 2026-01-12 21:25:22 -05:00
jbarciabf 36b45a64c5 rework 2026-01-12 21:08:19 -05:00
jbarciabf 333ca05a11 fixed error handling, added auto org detection for vpc, added features to whoami 2026-01-08 08:50:33 -05:00
jbarciabf 4a2444c05f Merge pull request #117 from BishopFox/readme-update
Update README.md
2026-01-05 09:05:08 -05:00
Seth Art c9edf8450b Update README.md 2026-01-05 09:03:09 -05:00
jbarciabf ec9a50ead1 fixed error handling 2026-01-04 09:46:15 -05:00
jbarciabf eae16c32b4 updated permission module 2025-12-30 13:54:10 -05:00
jbarciabf 0df2aa9240 updated to include all-projects and added project name to output 2025-12-30 13:32:16 -05:00
jbarciabf abf1543b3e added new modules 2025-12-30 09:38:32 -05:00
jbarciabf 69c9070009 updates 2025-12-28 15:23:44 -05:00
jbarciabf 8f6208d983 Merge remote-tracking branch 'upstream/main' into cloudfox-gcp-new 2025-12-17 14:52:35 -05:00
jbarciabf 83d9611732 init 2025-12-17 14:52:06 -05:00
jbarciabf b994b42708 Merge pull request #115 from BishopFox/fix/makefile-tab-signed3
fixed makefile
2025-12-17 13:01:58 -05:00
jbarciabf 52c12855a1 fixed makefile 2025-12-17 11:21:28 -05:00
jbarciabf 70037750c5 Merge pull request #109 from sethsec/fix_regions_error
Fix awsservicemap download, added more logging to all commands/sdk calls
2025-12-17 10:45:11 -05:00
Seth Art 1bb8576a27 Added logging for all successfull API calls to cloudfox-info.log 2025-12-15 13:33:55 -05:00
Seth Art 16b3c66624 Unified logging around logrus. removed standard logging lines. 2025-12-15 13:33:54 -05:00
Seth Art 3dbb98ffc6 Added split level logging to logrus 2025-12-15 13:33:54 -05:00
Seth Art b3a03ec7bd Fixed small bug with inventory where it was counting total resources incorrectly 2025-12-15 13:33:54 -05:00
Seth Art 521cf7e4f4 Reduced the number of times awsservicemap calls out to get the AWS json 2025-12-15 13:33:54 -05:00
jbarciabf 5115f849f5 Update CODEOWNERS 2025-08-05 15:57:12 -04:00
jbarciabf 9e42b53d22 Merge pull request #104 from sethsec/main
v1.16.0 - Addes multiple resource types to resource-trusts command thanks to @edops973
2025-08-05 15:50:49 -04:00
Bastien Faure b28ed6a3fd Update CODEOWNERS 2025-08-05 10:07:55 -04:00
Seth Art 0e7b006cde Merge branch 'main' into main 2025-04-21 11:49:06 -04:00
Seth Art 2cc97f508b Update version to 1.16.0 2025-04-21 11:43:07 -04:00
Seth Art 031d666c63 Merge pull request #6 from edops973/feature/add-opensearch-to-resource-trusts-command
feat: Add opensearch to resource trusts command
2025-04-21 10:19:43 -04:00
Seth Art d0de59dbe4 update opensearch mod 2025-04-21 10:19:06 -04:00
Seth Art 00b49aa33c Merge branch 'main' into feature/add-opensearch-to-resource-trusts-command 2025-04-16 19:15:07 -04:00
Seth Art 12ee6c292a Merge pull request #5 from edops973/feature/add-vpcendpoint-to-resource-trusts-command
feat: Add vpcendpoint to resource trusts command
2025-04-16 19:11:35 -04:00
Seth Art 4584774ec1 updated arn column to print full arn 2025-04-16 17:10:59 -06:00
Seth Art d558e1cf55 Update supported services banner 2025-04-16 16:56:11 -06:00
Seth Art 19b825cfa6 Merge remote-tracking branch 'origin' into feature/add-vpcendpoint-to-resource-trusts-command 2025-04-16 16:48:21 -06:00
Seth Art 698458cf92 Merge pull request #4 from edops973/feature/add-apigateway-service-to-resource-trusts-command
feat: Add apigateway service to resource trusts command
2025-04-16 18:43:00 -04:00
edops973 dfed0491f2 Ignore public apigateway in resource trusts command 2025-03-27 20:43:43 +07:00
edops973 7671e7e717 Add opensearch unittest 2025-03-15 22:53:01 +07:00
edops973 0c858e6cd4 Add missing method in codebuild_mocks.go 2025-03-15 22:47:53 +07:00
edops973 0deb568e41 Get open search policies 2025-03-15 22:47:43 +07:00
edops973 2806f7ea08 Merge conflict 2025-03-15 22:45:10 +07:00
edops973 e54ffe8ce7 Merge conflict 2025-03-15 22:43:21 +07:00
edops973 56e64d8e62 Merge conflict 2025-03-15 22:41:45 +07:00
edops973 43fe99e832 Merge conflict 2025-03-15 22:35:23 +07:00
edops973 5d2f2616af Merge conflict 2025-03-15 22:33:21 +07:00
edops973 d8e4157e93 Fix resource-trusts unittest 2025-03-15 22:15:04 +07:00
edops973 a104639354 Merge conflict 2025-03-15 22:01:17 +07:00
edops973 296710c474 Merge conflict 2025-03-15 21:56:19 +07:00
Seth Art 278e2658ee Merge pull request #2 from edops973/feature/add-kms-service-to-resource-trusts-command
feat: Add kms service to resource trusts command
2025-03-15 10:39:58 -04:00
Seth Art 6b45f54699 Removed block that ran kms even without feature flag. Also edited supported serviecs line to account for feature flag use 2025-03-15 10:38:38 -04:00
Seth Art 2391bb7802 merge updates 2025-03-15 10:31:23 -04:00
Seth Art 68d68fc75d fix typo in utils.go 2025-03-15 10:19:31 -04:00
edops973 7d6d16a4f1 Merge branch 'main' into feature/add-kms-service-to-resource-trusts-command 2025-03-14 06:04:02 -05:00
edops973 caaf399e9d Add feature flag for KMS service in resource-trusts command 2025-03-14 18:02:13 +07:00
edops973 1f4f128a7a Add checksum to the release packages 2025-03-14 18:02:12 +07:00
Seth Art 16c0f4dfe9 Bump version to v1.16.0-prerelease 2025-03-14 18:02:11 +07:00
edops973 7e6439924a Remove task definition column in env-vars command 2025-03-14 18:02:11 +07:00
edops973 d3c042c1e3 Add task definition column to env-vars command 2025-03-14 18:02:07 +07:00
edops973 cda06fff7a Add container name column to ecs-tasks command 2025-03-14 18:02:05 +07:00
edops973 e49402991a Fix the order of input parameters when fetching KMS policy 2025-03-14 18:01:59 +07:00
edops973 cae31fbf21 fix kms client nil when running all-checks command 2025-03-14 18:01:52 +07:00
edops973 4e75b1151b Add resource-trusts KMS unit test 2025-03-14 18:01:43 +07:00
edops973 831198c42d Support kms service for command resource-trusts 2025-03-14 18:01:35 +07:00
Bastien Faure 2cb5ad2b83 Merge pull request #97 from DominicBreuker/route53-bugfix
AWS Route53 Alias Record Support
2025-03-13 19:04:21 -04:00
Bastien Faure efe95c02ee Merge pull request #102 from crahan/linux-arm64
Add support for building Linux Arm64 binaries
2025-03-13 19:02:27 -04:00
CraHan 13bd04d3e4 Add support for building Linux Arm64 2025-03-13 19:01:26 -04:00
Seth Art b8d9d8093c Merge pull request #3 from edops973/feature/add-checksum-to-release
feat: Add checksum to the release packages
2025-03-12 16:26:11 -04:00
Seth Art b139513244 Merge pull request #1 from edops973/feature/add-taskdefinition-to-ecs-tasks-and-env-vars-command
feat: Add taskdefinition to ecs tasks and env vars command
2025-03-12 16:23:08 -04:00
Seth Art f058e1c57d Bump version to v1.16.0-prerelease 2025-03-12 16:21:54 -04:00
edops973 a94bdc1853 Remove task definition column in env-vars command 2025-03-13 02:45:39 +07:00
edops973 89b3492399 Fix the order of input parameters when fetching KMS policy 2025-03-13 00:00:16 +07:00
edops973 67b84f0c74 fix kms client nil when running all-checks command 2025-03-10 23:09:21 +07:00
edops973 8eec1c331b Add checksum to the release packages 2025-03-09 00:51:00 +07:00
edops973 5e0eb15f7d Add resource-trusts KMS unit test 2025-03-07 18:18:05 +07:00
edops973 ec497faba4 Support kms service for command resource-trusts 2025-03-07 15:25:53 +07:00
edops973 ed8e0a229c Add task definition column to env-vars command 2025-03-05 15:50:58 +07:00
edops973 b60f479c2d Add container name column to ecs-tasks command 2025-03-05 15:50:32 +07:00
Dominic Breuker ab4866fcbf fix bug with route53 alias record processing 2024-11-13 21:24:17 +01:00
Dominic Breuker 00fe4fdce3 add support for Route53 Alias records in route53 command 2024-10-30 21:22:22 +01:00
Bastien Faure f7d1cbcdb6 Merge pull request #96 from BishopFox/aws-principals-with-admin-check
Bug fixes and small feature updates
2024-10-17 10:47:11 +02:00
Seth Art b3c25bb5d0 spelling fix 2024-10-15 16:28:04 -04:00
Seth Art 9076beb111 Bumped version to 1.15.0 2024-10-15 16:26:04 -04:00
Seth Art 878d7ec0c4 Fixed bug in cape where files did not exist crash. Fixed bug that duplicated edges because of magenta. 2024-10-15 16:16:31 -04:00
Seth Art ebf9985d40 Fix for #92 2024-10-10 19:14:09 -04:00
Seth Art 8e1e8d0c0a Fix for cape that also makes edges for cross-account explicit trusts. Also added a new flag to ignore certain edges entirely. 2024-10-10 18:23:54 -04:00
Seth Art a88e98aedd Removed AWSSSO-ACCOUNTID entries from cape table as they are redundant 2024-09-30 16:05:49 -04:00
Seth Art 85b9b11798 Bumped version to 1.43.3 2024-09-24 16:17:49 -04:00
Seth Art 6b0970ebd7 Fix for cape in regards to permission expansion 2024-09-24 16:15:49 -04:00
Seth Art e0dc71b414 updated the adminActionNames check actions to remove ssm get documents and replace with ssm get parameters 2024-09-23 13:48:28 -04:00
Seth Art b31f36746b added the the --admin-check-only flag to iam-simulator command 2024-09-23 13:47:07 -04:00
Seth Art 0a95240b7f added admin/pmapper logic to principals command 2024-09-23 13:45:59 -04:00
David 785b69316a Merge pull request #93 from BishopFox/imds-fix
Fix for IMDS not working due to breaking change in aws-sdk-go-v2.
2024-08-09 00:19:47 -04:00
Seth Art 547c48a249 Merge branch 'main' into imds-fix 2024-08-07 21:42:54 -04:00
Seth Art aed3c0abdb bump version 2024-08-07 13:27:14 -04:00
Seth Art 0184e1520d Fix for IMDS not working due to breaking change in aws-sdk-go-v2. 2024-08-06 23:45:04 -04:00
David 70a050a973 Merge pull request #90 from BishopFox/add-386-binary-to-release
Update Makefile to include 386 linux binary for release action
2024-07-03 22:39:48 -04:00
David 738085dc78 fix typo from codespell 2024-07-03 17:59:19 -04:00
Seth Art 0ac24e3aa1 Update utils.go
bumping the version so that once this is merged and we can create a new tag and release on the new version
2024-07-02 11:48:36 -04:00
Seth Art 5ba8b087ff Update Makefile to include 386 linux binary for release action 2024-07-02 11:45:44 -04:00
Bastien Faure b02ad612f0 Merge pull request #89 from BishopFox/aws-databases-rds-fix
Aws databases rds fix
2024-06-03 19:47:52 -07:00
Bastien Faure 3759d2c57d Merge pull request #87 from testwill/close_file
fix: close input file
2024-06-03 19:46:25 -07:00
Seth Art 0b88bcac56 Updated tests to make sure that RDS instances without clusters are checked for 2024-05-29 11:30:49 -04:00
Seth Art 91f5f14f0e Added RDS database instances back into output. I think it's ok to have both clusters and instances in the output 2024-05-29 10:07:02 -04:00
guangwu ca810a00d7 fix: close input file 2024-05-11 20:43:07 +08:00
David d9cbefd911 Merge pull request #84 from BishopFox/graph
v1.14.0, cape
2024-04-16 14:41:05 -04:00
sethsec-bf a8d2bfbfb3 Removed graph command from cobra for now 2024-04-16 14:18:47 -04:00
Seth Art bfa95acafe Merge pull request #81 from BishopFox/bastien_directoryservice_aws
Add Directory Service support for AWS
2024-04-16 14:14:32 -04:00
sethsec-bf c0e4301ef3 Add GCP to readme, fix typo 2024-04-16 14:10:51 -04:00
sethsec-bf 22417f174b spelling 2024-04-16 11:54:10 -04:00
sethsec-bf c3be95b3db cleaned up enhanced pmapper loot file 2024-04-16 11:47:31 -04:00
sethsec-bf abcc930f20 added afero fs back to output2 (needed to pass brew tests) 2024-04-16 11:29:53 -04:00
sethsec-bf 77cd08b8d5 updated gcp verbosity, updated cape command usage, switched version tracking file from main.go to internal/utils.go 2024-04-16 11:24:18 -04:00
sethsec-bf 57194ef3e0 bump to version 1.14.0, merged gcp and aws ds functionality 2024-04-16 10:32:19 -04:00
sethsec-bf 82635fd44f made aws sso like eks, where edges are not created if it's if the provider is in the same account as the role that trusts it. the edges will still show up cross account though. 2024-04-16 10:28:15 -04:00
sethsec-bf 67af1bd7fb update gcp package with vuln 2024-04-16 10:23:06 -04:00
sethsec-bf 79f3899ead update go mod 2024-04-16 10:21:33 -04:00
Seth Art 8ab4c4200a Merge branch 'main' into bastien_directoryservice_aws 2024-04-15 20:30:27 -04:00
sethsec-bf 6e52e5ee7b merge gcp stuff from main into this branch 2024-04-15 16:33:09 -04:00
Joe 9920c411c4 Merge pull request #82 from BishopFox/codeowners
Update codeowners
2024-04-15 10:58:45 -07:00
moloch-- df3fb6eae0 Update codeowners 2024-04-15 10:58:21 -07:00
David 8e94b3c46a Merge pull request #79 from BishopFox/feature/gcp-v1
Cloudfox GCP Functionality
2024-04-12 11:43:43 -04:00
David 94628aae73 i cant spel 2024-04-12 01:12:19 +00:00
David 23e8878e49 Merge branch 'main' into feature/gcp-v1 2024-04-12 01:04:49 +00:00
sethsec-bf 356d57b2de Added pmapper basepath to all relevent commands. Improved logging for cape/cape-tui. Fixed codebuld cache bug. 2024-04-10 11:49:33 -04:00
sethsec-bf 3a45583015 Got cape working without any aws calls, cleaned up logging messages 2024-04-05 16:07:37 -04:00
Bastien Faure 57832ada7c Codespell fix 2024-04-02 12:01:31 -07:00
Bastien Faure 608f07804e AWS uses a mix of clouddirectory and directoryservices for Directory services 2024-04-02 12:00:45 -07:00
Bastien Faure de492fa6e9 Add Directory Service support for AWS 2024-04-02 11:49:45 -07:00
David dee22b3ebc fix logging issue and improve whoami 2024-03-29 19:44:17 +00:00
sethsec-bf 590a94b576 changed println to printf 2024-03-28 15:10:22 -04:00
sethsec-bf fca22ceb6b renamed to cape, added hop count logic, pulled privesc function out so i can add logic to handle cobra flags 2024-03-28 15:06:41 -04:00
sethsec-bf 84369247a7 quick fix for context error. logging issue still there. 2024-03-22 14:43:52 -04:00
sethsec-bf 4ef87d8b98 updated caper to use new version of parseFederatedRoleTrusts from the role-trusts command. Also changed the way vendors and federated identities are labled 2024-03-22 08:58:05 -04:00
David 334583d3a2 save work whomai account stub 2024-03-21 22:38:02 +00:00
sethsec-bf 8fac75ed11 merged from main 2024-03-21 09:58:35 -04:00
sethsec-bf 3ff704e98f merged from main 2024-03-21 09:56:18 -04:00
Seth Art b73cd90323 Merge pull request #78 from enzowritescode/feature/aws-neptune
Feature/aws neptune
2024-03-20 15:23:20 -04:00
sethsec-bf ce0310675d update release and fix database test 2024-03-20 15:22:04 -04:00
sethsec-bf f9d9ec4bb8 Merge branch 'main' of github.com:BishopFox/cloudfox into feature/aws-neptune 2024-03-20 15:18:22 -04:00
Seth Art 125c1ea0cb Merge pull request #80 from BishopFox/seth-dev
Fix API gatway bug and improve role trust processing
2024-03-15 16:09:34 -04:00
sethsec-bf 00e63b0e18 found a way to fix the apigateway types conflict with gob 2024-03-15 16:05:43 -04:00
sethsec-bf eaac503bff bumped version to 1.13.4 for release with apigateway fix 2024-03-15 11:35:29 -04:00
sethsec-bf cec0d49602 Fixing a bug in the new cached versions of the apigateway sdk calls 2024-03-15 11:28:35 -04:00
sethsec-bf 6a21b9665d Auth0 not ready yet - also this new version lists unknown federated types not instead of ignoring them 2024-03-14 14:49:04 -04:00
sethsec-bf a1903f67f7 Major update for role trusts parsing. Cleaner version has case statement on federated principal value and not soley on condition data 2024-03-14 14:47:17 -04:00
David 2f99a7642f working gcp functionality 2024-03-13 14:19:47 +00:00
sethsec-bf 8d039323bf Fix for #77 2024-03-08 09:01:59 -05:00
sethsec-bf 05fd899219 Fixed bug in federeated role trust poclies where multiple subjects are trusted 2024-03-07 15:56:13 -05:00
enzowritescode a40f0aa179 More cleanup 2024-02-28 15:05:38 -07:00
enzowritescode 23409e3b82 remove unused code 2024-02-28 14:59:50 -07:00
sethsec-bf 36c9a5b795 Merged changes from neptune PR into this branch 2024-02-28 09:34:41 -05:00
sethsec-bf fd01647a8a Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-02-28 09:32:25 -05:00
Seth Art f415f991c8 Merge pull request #74 from enzowritescode/feature/aws-neptune
Amazon Neptune Support
2024-02-27 12:24:16 -05:00
Seth Art 15e0507ad6 Merge branch 'main' into feature/aws-neptune 2024-02-27 10:13:10 -05:00
sethsec-bf 80c00f3d97 Added test for databases command 2024-02-27 10:10:56 -05:00
sethsec-bf 59afbb381e Switched RDS database command from instances to clusters, and since it grabs Neptune and DocsDB clusters, we don't need to run those api calls. (they all return the same data). Also added back port info and added role info to the RDS clusters in -o wide mode 2024-02-23 13:14:09 -05:00
enzowritescode ca437d3626 Filter Neptune results 2024-02-19 13:05:05 -07:00
sethsec-bf dd6dd291c5 playing around with saving graph state between runs 2024-02-13 16:20:37 -05:00
sethsec-bf 42b0f3a212 Merge branch 'main' of github.com:BishopFox/cloudfox into graph 2024-02-08 16:57:03 -05:00
sethsec-bf e4e548012f revert test 2024-02-08 16:56:51 -05:00
sethsec-bf 1ca5dbfb10 Fix bug in role-trusts command around new vendor lookup feature, enabled caching on apigateway commands 2024-02-08 16:49:50 -05:00
sethsec-bf 24b7076bc3 saving place in caper command 2024-02-08 16:32:15 -05:00
Seth Art f00044670b Merge pull request #75 from BishopFox/seth-dev
Version 1.13.2 - 2 bug fixes on new functionality in 1.13.1
2024-02-02 13:48:47 -05:00
sethsec-bf ae1dac6c21 Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-02-02 13:46:29 -05:00
sethsec-bf 237b0735a8 Bumped version to 1.13.2 2024-02-02 13:45:41 -05:00
sethsec-bf 18e38bf1ea Fixed bug in env-vars command introduced in 1.13.1 with the new interesting version of the table written to disk. was still recording them all. now the second table only has interesting env-vars 2024-02-02 13:42:37 -05:00
sethsec-bf b5908fc720 Fixed bug in the role trusts command introduced in 1.13.1 where cloudfox only shows princiapls with :root trust and not ALL role trusts 2024-02-02 13:29:33 -05:00
sethsec-bf e4f3421ea1 Added functionailty to hightlight admins in caper command 2024-02-02 13:23:49 -05:00
sethsec-bf e61a401733 Kept first draft as the graph command. Moved second take to the caper command. 2024-01-31 16:47:29 -05:00
enzowritescode 29514a96b5 Merge in latest and fix merge conflicts 2024-01-25 19:33:19 -07:00
Seth Art b2c450ee02 Merge pull request #73 from BishopFox/seth-dev
Release PR for 1.13.1
2024-01-25 20:31:07 -05:00
sethsec-bf a65610390b Bumped to version 1.31.1 before PR 2024-01-25 20:26:49 -05:00
sethsec-bf 997c13d4de Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-01-25 20:25:45 -05:00
sethsec-bf 1d1d198a01 More fixes from #72. Took the suggestion from @johnkeates and fixed the prepopulated nmap commands to NOT include a profile for the cases where the user did not specify a profile for cloudfox 2024-01-25 20:23:03 -05:00
sethsec-bf 6069267758 Fix for sub issue found by @cyberbutler in #72 2024-01-25 16:42:37 -05:00
sethsec-bf a13527a7a9 Added MakeVertices method for type Role 2024-01-25 16:25:11 -05:00
sethsec-bf e08967bb31 This potential fix for #72 makes a lot more sense. Rather than overrwite the profile attribute of the struct, i have an AWSProfileProvided and a AWSProfileFake so that I can just pass the orig to other modules (since each module cleans it up itself). 2024-01-25 15:47:38 -05:00
sethsec-bf aeccb6db6d Have global data in dom's graph format now. just need to write the table creation code 2024-01-25 15:24:49 -05:00
Seth Art 8e99347789 Update README.md 2024-01-23 21:50:00 -05:00
Seth Art ff810ba17d Update README.md 2024-01-23 09:50:26 -05:00
sethsec-bf 4d3c6a4c67 Merge remote-tracking branch 'origin/seth-dev' into graph 2024-01-22 21:26:47 -05:00
Seth Art 23fc346e4f Update README.md 2024-01-22 21:20:51 -05:00
sethsec-bf 89789a05c1 updated pmapper command info 2024-01-22 21:16:25 -05:00
sethsec-bf 5827abb2a2 Hopefully this is a fix for #72 2024-01-22 20:47:11 -05:00
sethsec-bf 13a03c5de2 Fixed panic bug that occured when user specified a profile that did not exist 2024-01-22 20:43:36 -05:00
sethsec-bf ac1125d47e added users model 2024-01-22 15:46:19 -05:00
sethsec-bf dd6bfefa9d Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-01-22 15:44:59 -05:00
sethsec-bf 009321e7cb Updated pmapper output files 2024-01-22 15:44:43 -05:00
enzowritescode 8c4bcc035b Add .idea to gitignore for GoLand 2024-01-20 19:55:15 -07:00
enzowritescode a38451c1cd Typo fixes, reduced copy pasta, Neptune support 2024-01-20 19:35:13 -07:00
sethsec-bf 4cf58a3479 Created loot file for pmapper 2024-01-18 10:06:41 -05:00
sethsec-bf f5437b7afb Started to add knownvendoraccounts info 2024-01-17 09:03:48 -05:00
Seth Art 5df1ca6d75 Update README.md 2024-01-06 10:23:22 -05:00
sethsec-bf ef25d8b04d Merged origin/seth-dev into graph 2024-01-03 09:10:16 -05:00
sethsec-bf ea2ee3dd2c Fixed bug where instances without instance profile were not showing up 2024-01-02 21:22:55 -05:00
sethsec-bf 97e2fef388 Add data from fwd:cloudsec's known_aws_accounts repo into role-trust module 2024-01-02 21:22:27 -05:00
sethsec-bf 1d53b984ac Used the output2 loot mechanism for api-gws, updated tests 2023-12-28 12:11:07 -05:00
sethsec-bf 525f262460 Added mocks for apigw and apigwv2, and a test for the new api-gw command 2023-12-27 14:54:03 -05:00
sethsec-bf f13df0708c Added mocks for apigw and apigwv2, and a test for the new api-gw command 2023-12-27 14:53:38 -05:00
Seth Art 7520999386 Merge pull request #67 from BishopFox/seth-dev
v1.13.0
2023-12-26 13:04:20 -05:00
sethsec-bf f1b6eae9f1 codespell was tripping up on a has from go.sum 2023-12-26 13:00:34 -05:00
sethsec-bf 06f8e36be2 bumped version to 1.13.0 2023-12-26 12:03:03 -05:00
sethsec-bf cba252d151 Normalize/clean up console messages. Also re-order the way the messages print in multi-profile mode 2023-12-26 12:02:26 -05:00
sethsec-bf 5c4d0fda89 go get -u and fixes 2023-12-26 10:54:39 -05:00
sethsec-bf d648e5b347 Added ability to supply just a princpal name, and the command will try it as a role and a user 2023-12-26 10:34:55 -05:00
sethsec-bf d1af1bbf8f Fixed a bug where permissions custom runs were overwriting the files for the default run. 2023-12-26 10:34:15 -05:00
sethsec-bf 2b19d0ac62 For 1.13, switching default AWS verbosity to 2 (with the exception of all-checks which will be hardocded to a versbosity of 1) 2023-12-26 10:20:04 -05:00
sethsec-bf cef6c9e656 Fixed a bug where iam-simulator custom commands were overwriting the files for the default run. 2023-12-26 10:09:29 -05:00
sethsec-bf 6e146a567a add mfatoken param from recent update to api-gw code 2023-12-26 09:40:25 -05:00
sethsec-bf ae7ed9e19a Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2023-12-24 10:16:36 -05:00
sethsec-bf a690ad3185 bumped version to 1.13.0-prerelease 2023-12-24 10:15:34 -05:00
Seth Art 26cb99778c Merge pull request #64 from wdahlenburg/api-gw
API Gateway Module
2023-12-24 10:13:43 -05:00
sethsec-bf 98af83933e Added intersting functionality to env-vars and workloads 2023-12-23 12:31:51 -05:00
sethsec-bf 82738dbf63 Fixed a bug where when pmapper data was missing iam simulator was not getting IAM roles properly. Also got workloads command working 2023-12-22 10:09:37 -05:00
sethsec-bf 96ab676f49 Feature: First draft of new workloads command - summarizes all workloads 2023-12-21 21:25:05 -05:00
sethsec-bf 83866f46cc Feature: Added a new output file for role trusts that just highlights the overly permissive root trusts without external ID 2023-12-21 21:24:01 -05:00
sethsec-bf 7343852a66 Updated endpoints to use the newly created sdk functions 2023-12-21 11:22:10 -05:00
sethsec-bf b1c248bbe6 Used SDK, added more counters
Moved all api calls to the sdk package.
Added counter to getEndpointsPerAPIGatewayv2 and
getEndpointsPerAPIGateway because in regions with lots of apigateways
this took a long time and it just hung on 66/68 tasks and i worried. The
additional counters will help give visabilty that things are still
moving along.
Added an additional alias api-gws
2023-12-21 10:18:00 -05:00
sethsec-bf 1e2eaf5576 merged from main 2023-12-19 20:15:14 -05:00
Seth Art 75a446764d Merge pull request #66 from BishopFox/seth-dev
Seth dev
2023-12-19 16:24:13 -05:00
sethsec-bf 66ffc57438 go mod tidy 2023-12-19 16:15:13 -05:00
sethsec-bf 9be3792c97 bumped version to 1.12.3 2023-12-13 10:07:23 -05:00
sethsec-bf 27a511073e for #65, decided to add a prompt to ask for MFA token if the user does not specify it at runtime 2023-12-13 10:05:11 -05:00
sethsec-bf a9a0c7ae30 more updates for #65. Added a cli flag --mfa-token so you can pass the token at the cloudfox command line. Ended up reworking AWSConfigFileLoader to use a map to store fetched configs which should have other performance improvemnts 2023-12-13 09:50:30 -05:00
sethsec-bf 2ed079686d fixed bug where glue resource policy data was not caching properly. 2023-12-13 09:48:15 -05:00
sethsec-bf 5e1ed54a28 fixed bug in cache loading that would error out if even a single cache file was not accounted for 2023-12-11 18:26:48 -05:00
sethsec-bf c4a276e4be graph/neo4j functionali working - detecting cross account attack paths 2023-12-11 18:24:34 -05:00
sethsec-bf ff38b2cf65 fix for #65 -- adds config.WithAssumeRoleCredentialOptions section to the LoadDefaultConfig loader 2023-12-06 15:58:04 -05:00
sethsec-bf 1819b11c5f Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2023-12-06 15:53:27 -05:00
sethsec-bf 4edbd3326c neo4j cross-account stuff kind of working, pmapper stuff not working 2023-12-06 15:52:49 -05:00
Wyatt Dahlenburg 2ce1f0f82f Codespell 2023-11-16 15:35:13 -07:00
Wyatt Dahlenburg 5eff1c71ac Merging with main + minor updates 2023-11-16 15:05:54 -07:00
Seth Art f51c1e9f49 Update README.md 2023-11-11 15:41:00 -05:00
sethsec-bf 8f5a571ef0 fixed issue where role trusts was not sorting services correctly anymore 2023-11-10 12:53:35 -05:00
sethsec-bf 5c40ef45df initial ideas for graph command 2023-11-09 08:23:37 -05:00
Seth Art 7b95a05215 Merge pull request #61 from BishopFox/seth-dev
ECR Fix and more
2023-11-02 13:50:48 -04:00
sethsec-bf e0f0627de8 new idea for resource trusts field 2023-11-02 10:39:30 -04:00
sethsec-bf d505bc4e3e Updating the tests/mocks for ECR 2023-11-02 09:46:59 -04:00
sethsec-bf 6a6f7586b3 Fixed a bad bug in ECR that was only looking at the fist repo 2023-11-01 20:28:36 -04:00
sethsec-bf 06344b9421 added secrets manager to resourcetrusts and cloud9 to inventory 2023-11-01 08:53:22 -04:00
sethsec-bf 54f784903a added accountID col to every table and removed the service col in all commands that are single service. 2023-10-26 12:39:25 -04:00
Seth Art 09f33f0908 Merge pull request #59 from BishopFox/seth-dev
Customizable Tables
2023-10-25 08:53:30 -04:00
sethsec-bf 2dc20eaa52 fixed codespell items 2023-10-25 08:50:43 -04:00
sethsec-bf 7656267367 fixes #36. -o wide will give you the arn, and the default (-o brief) will only give you the name for brevity 2023-10-25 08:41:04 -04:00
sethsec-bf b43d6ad074 go get -u and fixes 2023-10-25 08:19:44 -04:00
sethsec-bf 1215a27e19 completed --cols & -o wide support for ALL aws commands 2023-10-24 20:15:06 -04:00
sethsec-bf 7d3a61afe2 fixed caching issues in sdk/ecs 2023-10-24 13:06:08 -04:00
sethsec-bf 909590860d added user controled columns to cloudformation, and access keys 2023-10-23 16:28:22 -04:00
sethsec-bf 4ee0da839d added user controled columns to buckets, role-trusts, permissions 2023-10-23 16:00:48 -04:00
sethsec-bf 57d9e911f7 removed newline from buckets policy analyis 2023-10-20 12:30:39 -04:00
sethsec-bf dad2277942 moved policy analysis behind a feature flag in the buckets command 2023-10-19 10:04:53 -04:00
sethsec-bf 50493e6624 fixed bug in #53 - buckets with no policy were not creating policy cache stubs so each bucket without a policy would always look for a new policy 2023-10-11 12:12:22 -04:00
sethsec-bf 818c3a88f4 fixed cache load issue, updated aws instances command to use cached data 2023-09-22 15:16:38 -04:00
sethsec-bf bcfa318b31 cache load was loading json files along with the gob files and erroring. skipping the json files. also, updated ioutil library to os library 2023-09-20 13:22:22 -04:00
Seth Art f9567c7fc2 Merge pull request #56 from BishopFox/sethsec-bf-patch-1
Update README.md
2023-09-19 11:49:38 -04:00
Seth Art 187299ae82 Update README.md 2023-09-15 13:34:45 -04:00
Seth Art 8d145b135e Update README.md with more install methods 2023-09-08 08:48:25 -04:00
Seth Art 729c59e198 Merge pull request #52 from cclauss/codespell-github-action
GitHub Action to run codespell
2023-09-07 09:44:54 -04:00
Christian Clauss 6d1b47128e Update codespell.yml 2023-09-07 15:39:47 +02:00
Christian Clauss 217ac7a018 GitHub Action to run codespell 2023-09-07 15:38:03 +02:00
Seth Art 4b0238c362 Merge pull request #51 from cclauss/codespell
Fix typos discovered by codespell
2023-09-07 09:26:01 -04:00
Christian Clauss f78f1490d2 Fix typos discovered by codespell 2023-09-07 15:17:08 +02:00
Seth Art 0159638ab7 Merge pull request #50 from cclauss/patch-1
Upgrade GitHub Actions
2023-09-07 09:02:35 -04:00
Christian Clauss 1ff35a72ad Upgrade GitHub Actions
* https://github.com/actions/checkout/releases
* https://github.com/actions/setup-go/releases
2023-09-07 11:24:40 +02:00
Seth Art 29bfd2c863 Merge pull request #49 from BishopFox/seth-dev
bumped version to fix another local filesystem write
2023-09-06 14:05:38 -04:00
sethsec-bf 40e0c92ef1 another fix to pass brew tests 2023-09-06 14:04:50 -04:00
sethsec-bf 9f8ae08262 another fix to pass brew tests 2023-09-06 14:03:52 -04:00
Seth Art c85aa5de89 Merge pull request #48 from BishopFox/seth-dev
v1.12.1
2023-09-06 11:31:45 -04:00
sethsec-bf 42d7cfef1e hacky fix to have error log written to local dir if user dir does not exist (so it can pass brew test) 2023-09-05 18:04:25 -04:00
Seth Art a5bce5ad8c Merge pull request #46 from BishopFox/seth-dev
v1.12.0
2023-08-15 14:21:50 -04:00
sethsec-bf 8221b5ff6d bumped version to 1.12.0 2023-08-15 14:20:18 -04:00
Wyatt Dahlenburg 17011f345c Additional enumeration on API gateways compared to the endpoints module 2023-08-14 21:33:27 -06:00
sethsec-bf aa42c8b808 renamed azure instances -> vms, got userdata loot working on azure vms command 2023-08-14 13:43:13 -04:00
sethsec-bf 5aab3b4bea updated logging path for tenant data to make it more clear 2023-08-10 15:50:54 -04:00
sethsec-bf 01d320d64d inventory now supports tenant mode, both merged and split. fixed all tests 2023-08-10 14:54:25 -04:00
sethsec-bf 5132622a13 updated azure to use same home directory logic as the aws command 2023-08-10 14:53:22 -04:00
sethsec-bf 08b5cd17af renamed rbac and storage, and converted instances and storage over to user names in addition to IDs as input params. 2023-08-09 10:27:44 -04:00
sethsec-bf 102b91e2c7 Got rbac working with the new directory stucture and accepting common names for tentant and subscirption as input params 2023-08-08 13:02:49 -04:00
sethsec-bf 3a19fdb2fe reworking the tenant/subscription file structure 2023-07-24 09:50:59 -04:00
sethsec-bf d94fafe2b4 go get -u 2023-07-18 15:46:43 -04:00
sethsec-bf 00ed8a7336 Added many new resources to inventory. Made inventory rows dynamic. Only non-empty rows will print 2023-07-18 15:12:25 -04:00
sethsec-bf 60daff386c updated inventory to include more redshift, route53, updated route53 to use new cached functions and to include CNAME records in loot 2023-07-12 16:19:53 -04:00
sethsec-bf cef1aa3a82 updated inventory to include more ec2, ecs, and ecr resources 2023-07-11 09:30:09 -04:00
sethsec-bf e88cb77394 updated inventory to include more ec2, ecs, and ecr resources 2023-07-11 09:29:03 -04:00
sethsec-bf fc52e01192 updated tests, added new sdk functions, added started the migration of keeping all mocks in _mocks.go files 2023-07-10 09:37:06 -04:00
sethsec-bf 659b2e7248 Completed inventory migration to use sdk, also finally removed need for second global table 2023-07-06 22:50:44 -04:00
sethsec-bf 06a7e7ec28 Updated sdk package to do the gob initializations in self contained init functions 2023-07-06 14:35:57 -04:00
sethsec-bf 602332a0c5 Updated ecs-tasks and eni to use cached functions 2023-07-06 12:19:45 -04:00
sethsec-bf 471da59da2 Updated ecs-tasks and eni to use cached functions 2023-07-06 11:29:21 -04:00
Seth Art d4ae745e8e Merge pull request #45 from BishopFox/seth-dev
directory name for logging path
2023-06-28 11:39:47 -04:00
sethsec-bf dc0396ff33 updated logging path to have profile come before account-id to make it easier to find the right data 2023-06-28 11:37:45 -04:00
sethsec-bf c11ebf9a07 fixed newly introduced eks bug in the caching function (region and cluster name were passed in the wrong order) 2023-06-14 17:01:53 -04:00
sethsec-bf 6c8936cfe4 fixed two segfault issues due to not checking if fields within a struct are nil 2023-06-14 16:24:19 -04:00
Seth Art fca0c4da14 Merge pull request #44 from BishopFox/seth-dev
Switched default behavior on loading cached data from disk.
2023-06-14 09:49:58 -07:00
sethsec-bf 2c5fec49c3 loading cache from files by default was a bad idea. changed default to not load cache from disk, and if the -c flag is used, then cache is loaded from disk 2023-06-14 12:46:04 -04:00
sethsec-bf a9b95d9142 merged from main 2023-06-14 12:33:05 -04:00
Seth Art 0a74adea9a Merge pull request #43 from wdahlenburg/api-gw-patch
API Gateway Path Fix
2023-06-14 09:31:02 -07:00
Wyatt Dahlenburg adb9bcd55a Handle stage in apigatewayv2 2023-06-13 16:00:02 -06:00
Wyatt Dahlenburg 03177e8726 Fix paths on API Gateways & V2. Clean up path for S3 bucket endpoints as well 2023-06-13 15:01:54 -06:00
sethsec-bf 1fb9f4c3e0 fixed services table to sort by service which is way more helpful 2023-06-10 23:16:45 -04:00
Seth Art 3c6c59ec4b Update README.md 2023-06-09 14:33:51 -04:00
Seth Art 3f8b828a6c Merge pull request #41 from BishopFox/seth-dev
found and fixed some bugs in the new resource-trusts command
2023-06-09 14:32:01 -04:00
sethsec-bf b8bf12f1c4 found and fixed some bugs in the new resource-trusts command 2023-06-09 14:30:39 -04:00
sethsec-bf 690b3c8c77 found and fixed some bugs in the new resource-trusts command 2023-06-09 14:29:33 -04:00
Seth Art 845b69e9a0 Merge pull request #40 from BishopFox/seth-dev
updated lambda header to name instead of arn
2023-06-09 10:46:46 -04:00
sethsec-bf fc7a57129f updated lambda header to name instead of arn 2023-06-09 10:46:13 -04:00
Seth Art f6ec6a12f7 Merge pull request #39 from BishopFox/seth-dev
Version 1.11.0
2023-06-08 21:49:55 -04:00
sethsec-bf 56075588d5 bumped version to 1.11.0 2023-06-08 21:45:17 -04:00
sethsec-bf 7b51d9da96 updated tests 2023-06-08 21:43:42 -04:00
sethsec-bf a0a64c579c bumped go version for releaser 2023-06-08 21:43:20 -04:00
sethsec-bf 2b294e5845 updated wrapping for files written to disk, updated go modules 2023-06-08 21:33:20 -04:00
sethsec-bf d5956cebb0 tweak to database command cols 2023-06-08 21:05:40 -04:00
sethsec-bf 9581425231 updates to resource-trust command 2023-06-08 16:19:57 -04:00
sethsec-bf ce6c3f76fa migrated ecr and a few other functions in the endpoints and env-vars commands to use the cached functions 2023-06-03 12:41:10 -04:00
sethsec-bf 497c7940d2 migrated ecr to cached functions 2023-05-31 12:56:31 -04:00
sethsec-bf b29e4ea6b3 changed output directory, added beginning phases of cached support - both within one single run, but also loading cached data from previous runs, added codebuild command, added orgs command, added databases command, switched everything to use carlos's output2 functions, changed the output flag from type (csv,table) to be like kubectl with wide/narrow 2023-05-30 10:55:39 -04:00
sethsec-bf 2756cd0f45 changed default output dir to ~/.cloudfox/cloudfox-output 2023-05-23 15:32:43 -04:00
sethsec-bf c0597764a9 reworked the option to specify a principal so it works with groups also. i don't think it was working before 2023-05-18 15:58:27 -04:00
sethsec-bf ef1b37cef7 Added conditions col to the output to at least say if conditions exist on a permission 2023-05-10 20:34:03 -04:00
sethsec-bf 8e9bd42a1e finally added some logic so that if the user submits the name of the user/role/group instead of hte arn it will work anyway 2023-04-27 09:59:59 -04:00
sethsec-bf 8b2f13ca88 added ssm and ec2-instanceconnect commands to instances loot 2023-04-13 11:45:06 -04:00
sethsec-bf 8722782eae merged from main 2023-04-04 21:26:37 -04:00
sethsec-bf 0397b5df5f fixed bug in aws permissions - it was not handling policies applied to groups properly 2023-04-04 16:56:57 -04:00
sethsec-bf 5f7596b497 more draft work on the orgs command 2023-04-04 16:11:08 -04:00
sethsec-bf 0276ce25a4 initial draft of the orgs command 2023-03-30 07:35:48 -04:00
sethsec-bf 7490d1aaa1 fixed bug that wrote buckets, sns, sqs files in the wrong place when using environment variables 2023-03-28 15:18:01 -04:00
sethsec-bf 8ef71a6388 fixed bug in env-vars where sagaemaker model exists but primarycontainer does not 2023-03-27 13:44:48 -04:00
sethsec-bf 365286665c moved sns/sqs to another section in all-checks 2023-03-27 13:44:15 -04:00
Seth Art 537d064c6b Merge pull request #35 from motikan2010/main
bugfix 'SQS' and 'SNS' are inspected when 'all-checks' is executed.
2023-03-27 13:21:22 -04:00
sethsec-bf 766fe55484 added more to resource trusts module, started to move commands to shared initalizers 2023-03-26 15:15:31 -04:00
motikan2010 51f0b65087 bugfix 'SQS' and 'SNS' are inspected when 'all-checks' is executed. 2023-03-26 21:32:24 +09:00
sethsec-bf 186cd04405 Finished codebuild test, added codeguild to resource-trusts command, updated ecr test to add new function to the client interface 2023-03-21 12:09:45 -04:00
sethsec-bf 56247f40d4 first draft of resource-trusts command 2023-03-18 07:52:27 -04:00
sethsec-bf bc91da000c fixed bug in permissions command where it was choking on statements with conditions 2023-03-18 07:50:43 -04:00
Seth Art 02db8a4fe8 Merge pull request #33 from BishopFox/carlos-dev
azure bug fix
2023-03-13 16:44:24 -04:00
Seth Art dc7c8c6f68 Merge branch 'main' into carlos-dev 2023-03-13 16:43:46 -04:00
sethsec-bf 1aca8cf4cf fixed bug in filesystems command related mount targets 2023-03-13 13:22:58 -04:00
sethsec-bf 8550561cd7 fixed bug in filesystems command related to regions 2023-03-09 13:33:59 -05:00
sethsec-bf 7f413fc9d1 fixed bug in filesystems command 2023-03-09 13:31:34 -05:00
carlosvendramini b3620ca281 fixed output path in control messages for az inventory 2023-03-08 14:54:49 -05:00
carlosvendramini e6b5604a08 updated version tag to 1.10.1 2023-03-08 14:35:16 -05:00
carlosvendramini 47c11840d3 attempt to fix a bug within az validatePublicBlobURLs 2023-03-08 11:29:46 -05:00
carlosvendramini 77c7edfc82 fixed an error handling bug on the az storage and whoami commands 2023-03-08 11:17:13 -05:00
sethsec-bf cecebb82cb updated table file output to respect the wrap argument and the default which is to not wrap 2023-03-08 09:33:15 -05:00
sethsec-bf f2f93779ca updated inventory to include codebuild and started to break it out a bit into api call functions 2023-03-08 09:19:19 -05:00
sethsec-bf 59157b40d1 updated EFS to show file permissions and root directory info 2023-02-28 18:17:50 -05:00
Seth Art 07c4ddc755 Update README.md 2023-02-28 12:41:09 -05:00
carlosvendramini 05481ae748 az storage command now uses head method instead of get to check public blob URL 2023-02-28 12:06:12 -05:00
Seth Art 49eada30b6 Merge pull request #32 from BishopFox/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.2.0 to 0.7.0
2023-02-28 12:05:02 -05:00
dependabot[bot] bb1313c5c8 Bump golang.org/x/net from 0.2.0 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.2.0 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.2.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-02-28 17:03:36 +00:00
carlosvendramini fc736f4946 updated version tag to 1.10.0 2023-02-28 11:53:29 -05:00
carlosvendramini-bf d02b5e8f0e Merge pull request #31 from BishopFox/carlos-dev
Carlos dev
2023-02-28 11:49:47 -05:00
carlosvendramini 41be2192d3 added blobs test data 2023-02-28 11:45:43 -05:00
Seth Art 96587ff450 Update README.md 2023-02-28 09:37:31 -05:00
Seth Art 2e65bb319e added sqs and sns to readme 2023-02-23 12:13:15 -05:00
Seth Art fc3379e7ac Merge pull request #30 from BishopFox/seth-dev
Small improvements and fixes
2023-02-22 16:38:07 -05:00
sethsec-bf 2b686e9a3b Merge remote-tracking branch 'origin' into seth-dev 2023-02-22 16:28:48 -05:00
Seth Art 7300fc9eb3 Merge pull request #21 from DominicBreuker/sns-sqs-public-access-cmd
Add SNS and SQS commands with unit tests
2023-02-22 16:28:02 -05:00
sethsec-bf 303341c634 updated the buckets test to use the big table string as the comparable like sns/sqs 2023-02-21 16:48:03 -05:00
sethsec-bf 31027ba4cb updated all of the tests for sns/sqs/buckets to work with the new code 2023-02-21 16:05:10 -05:00
sethsec-bf 40933e2f81 fixed bug in permissions command where notaction permissions were being listed as allowed 2023-02-21 11:52:59 -05:00
carlosvendramini a8b73ef60d Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2023-02-21 06:43:29 -05:00
sethsec-bf b9f9a4fdab updated some iam-simulator default actions 2023-02-20 23:11:21 -05:00
sethsec-bf 3238ac866a switch awsservicemap behavior to pull latest servicemap from aws 2023-02-20 23:06:37 -05:00
sethsec-bf 2563ebbb72 removed pmapper column from output when pmapper data is not found 2023-02-20 22:53:18 -05:00
sethsec-bf 512602079b removed pmapper column from output when pmapper data is not found 2023-02-20 22:48:03 -05:00
sethsec-bf 152f7eaa8d added loot file to inventory which lists all identified arns 2023-02-20 22:26:01 -05:00
sethsec-bf 6aae684307 added test for tags command 2023-02-20 15:30:00 -05:00
sethsec-bf dbc9b1c2d3 added a max flag for the tags command because some accounts have hundreds of thousands of tags.
changed the behavior of the tags command in all-checks - it now sets the limit to 1000 resources per region,
and even if a user uses the -v2 or -v3 option, it overrides it to -v1 since it is so verbose
2023-02-20 11:30:28 -05:00
sethsec-bf 7c55958f54 added loot command output to sqs and sns 2023-02-17 22:49:27 -05:00
sethsec-bf ac21f3a8f0 finally got something worth pushing 2023-02-16 16:26:02 -05:00
sethsec-bf f207a7c095 updated sns/sqs/buckets commands - likely going to remove this 2023-02-14 18:16:26 -05:00
sethsec-bf cd66bbf52a updated sns/sqs/buckets commands - likely going to remove this 2023-02-14 18:11:29 -05:00
carlosvendramini dc7560c648 added az inventory per subscription 2023-02-10 14:51:46 -07:00
carlosvendramini 466c8fca53 small bug fix on output 2023-02-10 13:19:15 -07:00
carlosvendramini 2e8a454d04 output redesign ready for testing in prod 2023-02-10 13:06:22 -07:00
carlosvendramini 99b798b556 finished the az inventory module PoC code 2023-02-09 10:03:14 -05:00
carlosvendramini 78ff6651be finished new potential new output design, still need to discuss and test 2023-02-06 16:47:41 -05:00
carlosvendramini f1670f67da added new potential output design 2023-02-06 11:28:19 -05:00
carlosvendramini ba57a02a20 az blob enumeration complete, also added loot file with urls, need to add unit tests 2023-02-03 11:01:55 -05:00
carlosvendramini 6599ad2e83 public blob enumeration almost done, just need to add URLs to loot file 2023-02-02 17:45:37 -05:00
Seth Art 925014f001 Merge pull request #29 from BishopFox/seth-dev
Fix #28 and some other minor updates
2023-02-01 21:35:43 -05:00
sethsec-bf 7058d458e0 updated pmapper error state to point to wiki for next steps 2023-02-01 21:25:57 -05:00
sethsec-bf 8b5f79dc82 fixed pmapper path creation in linux to fix #28 2023-02-01 21:17:51 -05:00
sethsec-bf bb1f3388f3 added wiki link to network-ports command 2023-02-01 21:14:00 -05:00
carlosvendramini-bf 4081025480 wrote a prototype for az public blobs enumeration 2023-02-01 15:51:06 -05:00
sethsec-bf b64526f04f initial draft of adding more context to sqs table 2023-02-01 13:08:14 -05:00
Seth Art 78cdf2ef91 updated readme to include network-ports from #19 2023-01-31 13:48:36 -05:00
sethsec-bf 3d054eadee the test run complained about a fmt error in networkports 2023-01-31 12:27:18 -05:00
sethsec-bf 767d2e6bca added region check for each service to new commands 2023-01-31 12:21:30 -05:00
sethsec-bf 7ec51b1b96 incorporated new changes from v1.9.0 into this PR 2023-01-31 12:16:01 -05:00
Seth Art 10742b2f8b Merge pull request #19 from wdahlenburg/network-services
Identify Potentially Exposed Network Ports
2023-01-31 11:30:30 -05:00
sethsec-bf abeaa25922 bumping version and adding prerelease notation before pushing to main 2023-01-31 11:28:26 -05:00
Wyatt Dahlenburg 91aa229679 Restrict the processed security group rules to ALL, TCP, and UDP 2023-01-24 19:08:56 -07:00
sethsec-bf c265e5cb86 Merge branch 'network-services' of github.com:wdahlenburg/cloudfox into network-services 2023-01-23 17:34:12 -07:00
sethsec-bf f514b80f38 adding logic to ignore icmp 2023-01-23 17:33:52 -07:00
Wyatt Dahlenburg a0c8e4b2c4 Including ElastiCache in the cloudfox AWS policy 2023-01-23 07:50:17 -07:00
sethsec-bf 1c64fa50b9 added new 1.9.0 smart service logic 2023-01-22 23:15:39 -05:00
sethsec-bf 1ffd763a85 updated network-ports sections of cli/aws to incorporate new structure 2023-01-22 22:47:22 -05:00
sethsec-bf 5e87e968fd made some superficial updates before merge with main 2023-01-22 22:18:46 -05:00
Seth Art 12e9d5a3b1 Merge pull request #25 from JonZeolla/fix-typo
Fix ssm:SendCommand typo
2023-01-20 15:21:32 -05:00
Jon Zeolla 45cd84bcaa Fix ssm:SendCommand typo 2023-01-17 18:58:18 -05:00
Seth Art acabb09592 Update README.md 2023-01-14 21:05:11 -05:00
Seth Art b8515157f9 Update README.md 2023-01-14 21:04:28 -05:00
Seth Art 57aa072dda Update README.md 2023-01-11 08:53:02 -05:00
Seth Art dd688afd3e Updated README to split out Azure commands to their own table 2023-01-11 08:51:06 -05:00
Wyatt Dahlenburg 2b04fbb39a Merge branch 'main' into network-services 2023-01-04 20:29:19 -07:00
Wyatt Dahlenburg 6e9aa37d4a Refactor + Merge of Lightsail, ELBv2, ECS, ElastiCache, & EFS detections 2023-01-04 20:28:59 -07:00
Dominic Breuker 0ceaa0a767 use in-memory filesystem for unit tests 2022-12-12 21:14:53 +01:00
Dominic Breuker 01267d55e9 add SNS and SQS commands with unit tests 2022-12-09 22:12:05 +01:00
Wyatt Dahlenburg a09b549417 Merge branch 'main' into network-services 2022-11-27 11:22:14 -07:00
Wyatt Dahlenburg ca66af7e61 Basic Network ACL and Security Group parsing. Support for EC2 and RDS network ports. 2022-11-27 11:11:57 -07:00
364 changed files with 117531 additions and 6653 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
@sethsec-bf carlosvendramini-bf
* @bishopfaure @jbarciabf @msperling-bf
+6 -9
View File
@@ -15,10 +15,10 @@ jobs:
steps:
- name: Go 1.18
uses: actions/setup-go@v2
- name: Go 1.25
uses: actions/setup-go@v5
with:
go-version: ^1.18
go-version: ^1.25
id: go
- id: install-secret-key
@@ -28,7 +28,7 @@ jobs:
gpg --list-secret-keys --keyid-format LONG
- name: Check Out Code
uses: actions/checkout@v2
uses: actions/checkout@v6
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
@@ -39,9 +39,6 @@ jobs:
- name: Release binaries
uses: "marvinpinto/action-automatic-releases@latest"
uses: softprops/action-gh-release@v2
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: |
./cloudfox/*
files: ./cloudfox/*
+16
View File
@@ -0,0 +1,16 @@
# This Action uses minimal steps to run in ~5 seconds to rapidly:
# Looks for typos in the codebase using codespell
# https://github.com/codespell-project/codespell#readme
name: codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
codespell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: pip install --user codespell
- run: codespell --ignore-words-list="aks,referers,invokable" --skip="*.sum"
+13 -1
View File
@@ -15,6 +15,7 @@
# Dependency directories (remove the comment below to include it)
# vendor/
.vscode
.idea
launch.json
######################################################
@@ -61,9 +62,20 @@ terraform.rc
.DS_Store
untracked/*
*tmp*
tmp/*
output/*
*cloudfox-output*
cloudfox-*
cloudfox
*.log
*.bak
table_output.txt
*.txt
*.json
*.csv
*.log
dist/
# graphvis files
*.gv
*.svg
+34 -14
View File
@@ -13,6 +13,12 @@ linux:
zip cloudfox-linux.zip cloudfox sha1sum-linux.txt
rm cloudfox sha1sum-linux.txt
linux-arm64:
GOOS=linux GOARCH=arm64 go build
sha1sum cloudfox > sha1sum-linux-arm64.txt
zip cloudfox-linux-arm64.zip cloudfox sha1sum-linux-arm64.txt
rm cloudfox sha1sum-linux-arm64.txt
macos:
GOOS=darwin GOARCH=amd64 go build
sha1sum cloudfox > sha1sum-mac.txt
@@ -24,22 +30,36 @@ all: windows linux macos
.PHONY: release
release: clean
mkdir -p ./cloudfox
mkdir -p ./cloudfox
GOOS=windows GOARCH=amd64 go build -o ./cloudfox/cloudfox.exe .
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe
rm -rf ./cloudfox/cloudfox.exe
sha1sum ./cloudfox/cloudfox.exe > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox.exe ./cloudfox/sha1sum.txt
GOOS=linux GOARCH=amd64 go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox .
rm -rf ./cloudfox/cloudfox
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
GOOS=linux GOARCH=386 go build -o ./cloudfox/cloudfox .
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-linux-386.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
GOOS=linux GOARCH=arm64 go build -o ./cloudfox/cloudfox .
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-linux-arm64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
GOOS=darwin GOARCH=amd64 go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
clean:
rm -rf ./cloudfox
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
clean:
rm -rf ./cloudfox
+182 -17
View File
@@ -1,6 +1,10 @@
# :fox_face: CloudFox :fox_face:
CloudFox helps you gain situational awareness in unfamiliar cloud environments. It’s an open source command line tool created to help penetration testers and other offensive security professionals find exploitable attack paths in cloud infrastructure.
> **CLOUDFOX UPDATE REQUIRED: (12/2025):** If you are using `cloudfox`, you need to use v1.17.0 or greater. All earlier versions [stopped working after a format change in AWS's public service mapping file](https://github.com/BishopFox/cloudfox/pull/109).
**Overview**
#### CloudFox helps you answer the following common questions (and many more):
* What regions is this AWS account using and roughly how many resources are in the account?
@@ -14,8 +18,17 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
## Demos, Examples, Walkthroughs
* [Blog - Introducing: CloudFox](https://bishopfox.com/blog/introducing-cloudfox)
* [Video - CloudFox Intro Demo's](https://www.youtube.com/watch?v=ReWoUgpUuiQ)
* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360)
* [Video - CloudFox + CloudFoxable A Powerful Duo for Mastering the Art of Identifying and Exploiting AWS Attack Paths](https://www.youtube.com/watch?v=RdQiIvCrSzk)
* [Video - Penetration Testing with CloudFox](https://www.youtube.com/watch?v=Ljt_JUp5HbM)
* [Video - CloudFox Intro Demos](https://www.youtube.com/watch?v=ReWoUgpUuiQ)
* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360)
## Intentionally Vulnerable Playground
* [CloudFoxable - A Gamified Cloud Hacking Sandbox](https://cloudfoxable.bishopfox.com/)
## Want to chat about CloudFox?
Join us on the [RedSec discord server](https://discord.gg/redsec)
## Quick Start
CloudFox is modular (you can run one command at a time), but there is an `aws all-checks` command that will run the other aws commands for you with sane defaults:
@@ -29,8 +42,9 @@ CloudFox is modular (you can run one command at a time), but there is an `aws al
CloudFox was designed to be executed by a principal with limited read-only permissions, but it's purpose is to help you find attack paths that can be exploited in simulated compromise scenarios (aka, objective based penetration testing).
### Black Box Enumeration
CloudFox can be with "found" credentials, similar to how you would use [weirdAAL](https://github.com/carnal0wnage/weirdAAL) or [enumerate-iam](https://github.com/andresriancho/enumerate-iam). Checks that fail, do so silently, so any data returned means your "found" creds have the access needed to retrieve it.
CloudFox can be used with "found" credentials, similar to how you would use [weirdAAL](https://github.com/carnal0wnage/weirdAAL) or [enumerate-iam](https://github.com/andresriancho/enumerate-iam). Checks that fail, do so silently, so any data returned means your "found" creds have the access needed to retrieve it.
### Documentation
For the full documentation please refer to our [wiki](https://github.com/BishopFox/CloudFox/wiki).
@@ -38,9 +52,9 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
| Provider| CloudFox Commands |
| - | - |
| AWS | 23 |
| Azure | 4 |
| GCP | Support Planned |
| AWS | 34 |
| Azure | 4 |
| GCP | 60 |
| Kubernetes | Support Planned |
@@ -48,22 +62,36 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
**Option 1:** Download the [latest binary release](https://github.com/BishopFox/cloudfox/releases) for your platform.
**Option 2:** [Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source
**Option 2:** If you use homebrew: `brew install cloudfox`
**Option 3:** [Install Go](https://golang.org/doc/install), use `go install github.com/BishopFox/cloudfox@latest` to install from the remote source
**Option 4:** Developer mode:
[Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source
```
# git clone https://github.com/BishopFox/cloudfox.git
...omitted for brevity...
# cd ./cloudfox
# Make any changes necessary
# go build .
# ./cloudfox
```
**Option 5:** Testing a bug fix
```
git clone [email protected]:BishopFox/cloudfox.git
git checkout seth-dev
go build .
./cloudfox [rest of the command options]
```
# Prerequisites
### AWS
* AWS CLI installed
* Supports AWS profiles, AWS environment variables, or metadata retrieval (on an ec2 instance)
* To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names seperated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag.
* To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names separated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag.
* A principal with one recommended policies attached (described below)
* Recommended attached policies: **`SecurityAudit` + [CloudFox custom policy](./misc/aws/cloudfox-policy.json)**
@@ -78,15 +106,65 @@ Additional policy notes (as of 09/2022):
| `arn:aws:iam::aws:policy/AdministratorAccess` | This will work just fine with CloudFox, but if you were handed this level of access as a penetration tester, that should probably be a finding in itself :) |
### Azure
* Viewer or similar permissions applied.
* Viewer or similar permissions applied.
# Supported Commands
### GCP
* Google Cloud SDK installed and authenticated
* Application Default Credentials configured (`gcloud auth application-default login`)
* Recommended permissions at appropriate hierarchy levels (see below)
#### GCP Permissions: Minimal vs Comprehensive
**Minimal Permissions (Single Project):**
For basic enumeration of a single project, the `roles/viewer` role provides read access to most resources (includes logging, monitoring, and compute/network viewing).
**Comprehensive Permissions (Organization-Wide):**
For thorough security assessments across an entire organization:
| Scope | Role | Purpose |
| - | - | - |
| **Organization** | `roles/resourcemanager.organizationViewer` | View organization structure and metadata |
| **Organization** | `roles/iam.securityReviewer` | Review IAM policies across the organization |
| **Organization** | `roles/cloudasset.viewer` | Query Cloud Asset Inventory for all resources |
| **Organization** | `roles/cloudidentity.groupsViewer` | Enumerate Google Groups and memberships |
| **Folder** | `roles/resourcemanager.folderViewer` | View folder hierarchy and metadata |
| **Project** | `roles/viewer` | Read access to most project resources (includes logging.viewer, monitoring.viewer, compute.viewer) |
| **Tooling Project** | `roles/serviceusage.serviceUsageAdmin` | (Optional) Manage API quotas for CloudFox operations |
> **Note:** The basic `roles/viewer` role includes permissions from `roles/logging.viewer`, `roles/monitoring.viewer`, and `roles/compute.networkViewer`, so these don't need to be granted separately.
#### GCP API Requirements
**APIs must be enabled in each project you want to assess.** GCP APIs are project-scoped.
| API | Service Name | Purpose |
| - | - | - |
| Cloud Identity API | `cloudidentity.googleapis.com` | Group enumeration, inherited role analysis |
| Cloud Asset API | `cloudasset.googleapis.com` | Cross-project resource discovery |
| Cloud Resource Manager API | `cloudresourcemanager.googleapis.com` | Organization mapping, IAM enumeration |
| IAM API | `iam.googleapis.com` | IAM analysis, privilege escalation detection |
| Compute Engine API | `compute.googleapis.com` | Instance enumeration, network security |
| Secret Manager API | `secretmanager.googleapis.com` | Secrets enumeration |
| Cloud Functions API | `cloudfunctions.googleapis.com` | Serverless enumeration |
| Cloud Run API | `run.googleapis.com` | Serverless enumeration |
| Kubernetes Engine API | `container.googleapis.com` | Container security analysis |
| BigQuery API | `bigquery.googleapis.com` | Data security analysis |
For detailed setup instructions, see the [GCP Setup Guide](https://github.com/BishopFox/cloudfox/wiki/GCP-Setup-Guide).
# AWS Commands
| Provider | Command Name | Description
| - | - | - |
| AWS | [all-checks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#all-checks) | Run all of the other commands using reasonable defaults. You'll still want to check out the non-default options of each command, but this is a great place to start. |
| AWS | [access-keys](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#access-keys) | Lists active access keys for all users. Useful for cross referencing a key you found with which in-scope account it belongs to. |
| AWS | [api-gw](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#api-gw) | Lists API gateway endpoints and gives you custom curl commands including API tokens if they are stored in metadata. |
| AWS | [buckets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#filesystems) | Lists the buckets in the account and gives you handy commands for inspecting them further. |
| AWS | [cape](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cape) | Enumerates cross-account privilege escalation paths. Requires `pmapper` to be run first |
| AWS | [cloudformation](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cloudformation) | Lists the cloudformation stacks in the account. Generates loot file with stack details, stack parameters, and stack output - look for secrets. |
| AWS | [codebuild](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#codebuild) | Enumerate CodeBuild projects |
| AWS | [databases](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#databases) | Enumerate RDS databases. Get a loot file with connection strings. |
| AWS | [ecr](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecr) | List the most recently pushed image URI from all repositories. Use the loot file to pull selected images down with docker/nerdctl for inspection. |
| AWS | [ecs-tasks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecs-tasks) | List all ecs tasks. This returns a list of ecs tasks and associated cluster, task definition, container instance, launch type, and associated IAM principal. |
| AWS | [eks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#eks) | List all EKS clusters, see if they expose their endpoint publicly, and check the associated IAM roles attached to reach cluster or node group. Generates a loot file with the `aws eks udpate-kubeconfig` command needed to connect to each cluster. |
@@ -98,28 +176,115 @@ Additional policy notes (as of 09/2022):
| AWS | [instances](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#instances) | Enumerates useful information for EC2 Instances in all regions like name, public/private IPs, and instance profiles. Generates loot files you can feed to nmap and other tools for service enumeration. |
| AWS | [inventory](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#inventory) | Gain a rough understanding of size of the account and preferred regions. |
| AWS | [lambda](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#lambda) | Lists the lambda functions in the account, including which one's have admin roles attached. Also gives you handy commands for downloading each function. |
| AWS | [network-ports](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#network-ports) | Enumerates AWS services that are potentially exposing a network service. The security groups and the network ACLs are parsed for each resource to determine what ports are potentially exposed. |
| AWS | [orgs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#orgs) | Enumerate accounts in an organization |
| AWS | [outbound-assumed-roles](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
| AWS | [permissions](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#permissions) | Enumerates IAM permissions associated with all users and roles. Grep this output to figure out what permissions a particular principal has rather than logging into the AWS console and painstakingly expanding each policy attached to the principal you are investigating. |
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
| AWS | [pmapper](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#pmapper) | Looks for pmapper data stored on the local filesystem, [in the locations defined here](https://github.com/nccgroup/PMapper/wiki/Frequently-Asked-Questions#where-does-pmapper-store-its-data). If pmapper data has been found (you already ran `pmapper graph create`), then this command will use this data to build a graph in cloudfox memory let you know who can privesc to admin.
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
| AWS | [ram](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ram) | List all resources in this account that are shared with other accounts, or resources from other accounts that are shared with this account. Useful for cross-account attack paths. |
| AWS | [resource-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#resource-trusts) | Looks through multiple services that support resource policies and helps you find any overly permissive resource trusts. KMS is supported but disabled by default. To include KMS resource policies in the output, add this flag to the command: `cloudfox aws resource-trusts --include-kms`.|
| AWS | [role-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#role-trusts) | Enumerates IAM role trust policies so you can look for overly permissive role trusts or find roles that trust a specific service. |
| AWS | [route53](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#route53) | Enumerate all records from all route53 managed zones. Use this for application and service enumeration. |
| AWS | [secrets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#secrets) | List secrets from SecretsManager and SSM. Look for interesting secrets in the list and then see who has access to them using use `cloudfox iam-simulator` and/or `pmapper`. |
| AWS | [sns](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sns) | This command enumerates all of the sns topics and gives you the commands to subscribe to a topic or send messages to a topic (if you have the permissions needed). This command only deals with topics, and not the SMS functionality. This command also attempts to summarize topic resource policies if they exist.|
| AWS | [sqs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sqs) | This command enumerates all of the sqs queues and gives you the commands to receive messages from a queue and send messages to a queue (if you have the permissions needed). This command also attempts to summarize queue resource policies if they exist.|
| AWS | [tags](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#tags) | List all resources with tags, and all of the tags. This can be used similar to inventory as another method to identify what types of resources exist in an account. |
| Azure | [instances-map](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#instances-map) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
| Azure | [rbac-map](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac-map) | Enumerates Role Assignments for all tenants |
| AWS | [workloads](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the compute workloads and what role they have. Tells you if any of the roles are admin (bad) and if you have pmapper data locally, it will tell you if any of the roles can privesc to admin (also bad) |
| AWS | [ds](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the AWS-managed directories and their attributes. Also summarizes the current trusts with their directions and types. |
# Azure Commands
| Provider | Command Name | Description
| - | - | - |
| Azure | [whoami](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#whoami) | Displays information on the tenant, subscriptions and resource groups available to your current Azure CLI session. This is useful to provide situation awareness on what tenant and subscription IDs to use with the other sub commands. |
| Azure | [inventory](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#inventory) | Display an inventory table of all resources per location. |
| Azure | [rbac](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac) | Lists Azure RBAC role assignments at subscription or tenant level |
| Azure | [storage](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#storage) | The storage command is still under development. Currently it only displays limited data about the storage accounts |
| Azure | [vms](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#vms) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
# GCP Commands
For detailed documentation on each GCP command, see the [GCP Commands Wiki](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands).
| Provider | Command Name | Description
| - | - | - |
| GCP | [whoami](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#whoami) | Display identity context for the authenticated GCP user/service account |
| GCP | [iam](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#iam) | Enumerate GCP IAM principals across organizations, folders, and projects |
| GCP | [permissions](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#permissions) | Enumerate ALL permissions for each IAM entity with full inheritance explosion |
| GCP | [serviceaccounts](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#serviceaccounts) | Enumerate GCP service accounts with security analysis |
| GCP | [service-agents](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#service-agents) | Enumerate Google-managed service agents |
| GCP | [keys](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#keys) | Enumerate all GCP keys (SA keys, HMAC keys, API keys) |
| GCP | [resource-iam](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#resource-iam) | Enumerate IAM policies on GCP resources (buckets, datasets, secrets, etc.) |
| GCP | [domain-wide-delegation](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#domain-wide-delegation) | Find service accounts with Domain-Wide Delegation to Google Workspace |
| GCP | [privesc](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#privesc) | Identify privilege escalation paths in GCP projects |
| GCP | [hidden-admins](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#hidden-admins) | Identify principals who can modify IAM policies (hidden admins) |
| GCP | [identity-federation](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#identity-federation) | Enumerate Workload Identity Federation (external identities) |
| GCP | [instances](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#instances) | Enumerate GCP Compute Engine instances with security configuration |
| GCP | [gke](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#gke) | Enumerate GKE clusters with security analysis |
| GCP | [cloudrun](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloudrun) | Enumerate Cloud Run services and jobs with security analysis |
| GCP | [functions](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#functions) | Enumerate GCP Cloud Functions with security analysis |
| GCP | [app-engine](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#app-engine) | Enumerate App Engine applications and security configurations |
| GCP | [composer](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#composer) | Enumerate Cloud Composer environments |
| GCP | [dataproc](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#dataproc) | Enumerate Dataproc clusters |
| GCP | [dataflow](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#dataflow) | Enumerate Dataflow jobs and pipelines |
| GCP | [notebooks](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#notebooks) | Enumerate Vertex AI Workbench notebooks |
| GCP | [workload-identity](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#workload-identity) | Enumerate GKE Workload Identity and Workload Identity Federation |
| GCP | [inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#inventory) | Quick resource inventory - works without Cloud Asset API |
| GCP | [storage](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#storage) | Enumerate GCP Cloud Storage buckets with security configuration |
| GCP | [storage-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#storage-enum) | Enumerate GCS buckets for sensitive files (credentials, secrets, configs) |
| GCP | [bigquery](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigquery) | Enumerate GCP BigQuery datasets and tables with security analysis |
| GCP | [cloudsql](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloudsql) | Enumerate Cloud SQL instances with security analysis |
| GCP | [spanner](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#spanner) | Enumerate Cloud Spanner instances and databases |
| GCP | [bigtable](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigtable) | Enumerate Cloud Bigtable instances and tables |
| GCP | [filestore](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#filestore) | Enumerate Filestore NFS instances |
| GCP | [memorystore](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#memorystore) | Enumerate Memorystore (Redis) instances |
| GCP | [vpc-networks](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#vpc-networks) | Enumerate VPC Networks |
| GCP | [firewall](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#firewall) | Enumerate VPC networks and firewall rules with security analysis |
| GCP | [loadbalancers](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#loadbalancers) | Enumerate Load Balancers |
| GCP | [dns](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#dns) | Enumerate Cloud DNS zones and records with security analysis |
| GCP | [endpoints](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#endpoints) | Enumerate all network endpoints (external and internal) with IPs, ports, and hostnames |
| GCP | [private-service-connect](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#private-service-connect) | Enumerate Private Service Connect endpoints and service attachments |
| GCP | [network-topology](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#network-topology) | Visualize VPC network topology, peering relationships, and trust boundaries |
| GCP | [vpc-sc](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#vpc-sc) | Enumerate VPC Service Controls |
| GCP | [access-levels](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#access-levels) | Enumerate Access Context Manager access levels |
| GCP | [cloud-armor](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloud-armor) | Enumerate Cloud Armor security policies and find weaknesses |
| GCP | [iap](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#iap) | Enumerate Identity-Aware Proxy configurations |
| GCP | [beyondcorp](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#beyondcorp) | Enumerate BeyondCorp Enterprise configurations |
| GCP | [kms](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#kms) | Enumerate Cloud KMS key rings and crypto keys with security analysis |
| GCP | [secrets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#secrets) | Enumerate GCP Secret Manager secrets with security configuration |
| GCP | [cert-manager](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cert-manager) | Enumerate SSL/TLS certificates and find expiring or misconfigured certs |
| GCP | [org-policies](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#org-policies) | Enumerate organization policies and identify security weaknesses |
| GCP | [artifact-registry](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#artifact-registry) | Enumerate GCP Artifact Registry and Container Registry with security configuration |
| GCP | [cloudbuild](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloudbuild) | Enumerate Cloud Build triggers and builds |
| GCP | [source-repos](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#source-repos) | Enumerate Cloud Source Repositories |
| GCP | [scheduler](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#scheduler) | Enumerate Cloud Scheduler jobs with security analysis |
| GCP | [pubsub](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#pubsub) | Enumerate Pub/Sub topics and subscriptions with security analysis |
| GCP | [logging](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#logging) | Enumerate Cloud Logging sinks and metrics with security analysis |
| GCP | [organizations](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#organizations) | Enumerate GCP organization hierarchy |
| GCP | [asset-inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#asset-inventory) | Enumerate Cloud Asset Inventory with optional dependency analysis |
| GCP | [backup-inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#backup-inventory) | Enumerate backup policies, protected resources, and identify backup gaps |
| GCP | [lateral-movement](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#lateral-movement) | Map lateral movement paths, credential theft vectors, and pivot opportunities |
| GCP | [data-exfiltration](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#data-exfiltration) | Identify data exfiltration paths, potential vectors, and missing security hardening |
| GCP | [public-access](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#public-access) | Find resources with allUsers/allAuthenticatedUsers access across 16 GCP services |
| GCP | [cross-project](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cross-project) | Analyze cross-project IAM bindings, logging sinks, and Pub/Sub exports for lateral movement |
| GCP | [foxmapper](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#foxmapper) | Run FoxMapper (graph-based IAM analysis) for privilege escalation path discovery |
| GCP | [logging-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#logging-enum) | Scan Cloud Logging entries for sensitive data (credentials, tokens, PII) |
| GCP | [bigquery-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigquery-enum) | Scan BigQuery datasets, tables, and columns for sensitive data indicators |
| GCP | [bigtable-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigtable-enum) | Scan Bigtable instances, tables, and column families for sensitive data indicators |
| GCP | [spanner-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#spanner-enum) | Scan Spanner database schemas for sensitive table and column names |
# Authors
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
* [Seth Art (@sethsec](https://twitter.com/sethsec))
* Joseph Barcia
# Contributing
[Wiki - How to Contribute](https://github.com/BishopFox/cloudfox/wiki#how-to-contribute)
# TODO
* AWS - Add support for GovCloud and China regions
# FAQ
@@ -129,7 +294,7 @@ CloudFox doesn't create any alerts or findings, and doesn't check your environme
**Why do I see errors in some CloudFox commands?**
* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't suppor the service will return errors.
* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't support the service will return errors.
* You don't have permission - Another reason you might see errors if you don't have permissions to make calls that CloudFox is making. Either because the policy doesn't allow it (e.g., SecurityAudit doesn't allow all of the permissions CloudFox needs. Or, it might be an SCP that is blocking you.
You can always look in the ~/.cloudfox/cloudfox-error.log file to get more information on errors.
+71 -49
View File
@@ -1,27 +1,28 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type AccessKeysModule struct {
// General configuration data
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSProfile string
OutputFormat string
Goroutines int
WrapTable bool
AWSOutputType string
AWSTableCols string
CommandCounter internal.CommandCounter
// Main module data
@@ -37,8 +38,8 @@ type UserKeys struct {
Key string
}
func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "access-keys"
@@ -56,6 +57,7 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
// Variables used to draw table output
m.output.Headers = []string{
"Account",
"User Name",
"Access Key ID",
}
@@ -66,6 +68,7 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
key.Username,
key.Key,
},
@@ -79,12 +82,51 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
fmt.Printf("[%s][%s] Only active access keys are shown.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
//fmt.Printf("[%s][%s] Preparing output.\n\n")
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
m.writeLoot(m.output.FilePath, verbosity)
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"User Name",
"Access Key ID",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"User Name",
"Access Key ID",
}
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
//m.writeLoot(m.output.FilePath, verbosity)
fmt.Printf("[%s][%s] %s access keys found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No access keys found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -116,50 +158,30 @@ func (m *AccessKeysModule) writeLoot(outputDirectory string, verbosity int) {
func (m *AccessKeysModule) getAccessKeysForAllUsers() {
// The "PaginationControl" value remains true until all data is received (default is 100 results per page).
// "" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
PaginationControl := true
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
for PaginationControl {
// added this to break out if there no users
if len(ListUsers) != 0 {
for _, user := range ListUsers {
IAMQuery, err := m.IAMClient.ListUsers(context.TODO(), &iam.ListUsersInput{Marker: PaginationMarker})
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
results, err := sdk.CachedIamListAccessKeys(m.IAMClient, aws.ToString(m.Caller.Account), aws.ToString(user.UserName))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
break
}
// added this to break out if there no users
if len(IAMQuery.Users) == 0 {
break
}
// This for loop extracts relevant information for each access key and adds to output array
for _, key := range results {
// This for loop iterates through all users.
for PaginationControl {
PaginationMarker = nil
for _, user := range IAMQuery.Users {
results, err := m.IAMClient.ListAccessKeys(context.TODO(), &iam.ListAccessKeysInput{UserName: user.UserName, Marker: PaginationMarker})
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
if key.Status == "Active" {
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
}
// This for loop extracts relevant information for each access key and adds to output array
for _, key := range results.AccessKeyMetadata {
if key.Status == "Active" {
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
}
}
PaginationControl = results.IsTruncated
PaginationMarker = results.Marker
}
}
PaginationControl = IAMQuery.IsTruncated
PaginationMarker = IAMQuery.Marker
}
}
+686
View File
@@ -0,0 +1,686 @@
package aws
import (
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
apigatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
apigatewayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
var CURL_COMMAND string = "curl -X %s %s"
type ApiGwModule struct {
// General configuration data
APIGatewayClient sdk.APIGatewayClientInterface
APIGatewayv2Client sdk.APIGatewayv2ClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Gateways []ApiGateway
CommandCounter internal.CommandCounter
Errors []string
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type ApiGateway struct {
AWSService string
Region string
Name string
Endpoint string
ApiKey string
Public string
Method string
}
func (m *ApiGwModule) PrintApiGws(outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "api-gw"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s][%s] Enumerating api-gateways for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan ApiGateway)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
//execute regional checks
for _, region := range m.AWSRegions {
wg.Add(1)
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
wg.Wait()
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
sort.Slice(m.Gateways, func(i, j int) bool {
return m.Gateways[i].AWSService < m.Gateways[j].AWSService
})
m.output.Headers = []string{
"Service",
"Region",
"Name",
"Method",
"Endpoint",
"ApiKey",
"Public",
}
// Table rows
for i := range m.Gateways {
m.output.Body = append(
m.output.Body,
[]string{
m.Gateways[i].AWSService,
m.Gateways[i].Region,
m.Gateways[i].Name,
m.Gateways[i].Method,
m.Gateways[i].Endpoint,
m.Gateways[i].ApiKey,
m.Gateways[i].Public,
},
)
}
if len(m.output.Body) > 0 {
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
DirectoryName: filepath,
},
Loot: internal.LootClient{
DirectoryName: filepath,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
loot := m.writeLoot(filepath, verbosity)
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
Name: m.output.CallingModule,
Contents: loot,
})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
fmt.Printf("[%s][%s] %s API gateways found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No API gateways found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *ApiGwModule) Receiver(receiver chan ApiGateway, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Gateways = append(m.Gateways, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *ApiGwModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer wg.Done()
// check the concurrency semaphore
// semaphore <- struct{}{}
// defer func() {
// <-semaphore
// }()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("api-gateway", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayAPIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayVIPsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayv2VIPsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) string {
path := filepath.Join(outputDirectory, "loot")
f := filepath.Join(path, "api-gws.txt")
var out string
for _, endpoint := range m.Gateways {
method := endpoint.Method
// Write a GET and POST for ANY
if endpoint.Method == "ANY" {
line := fmt.Sprintf(CURL_COMMAND, "GET", endpoint.Endpoint)
if endpoint.ApiKey != "" {
line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey)
}
out += line + "\n"
method = "POST"
}
line := fmt.Sprintf(CURL_COMMAND, method, endpoint.Endpoint)
if endpoint.ApiKey != "" {
line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey)
}
if method == "DELETE" || method == "PATCH" || method == "POST" || method == "PUT" {
line += " -H 'Content-Type: application/json' -d '{}'"
}
out += line + "\n"
}
// err = os.WriteFile(f, []byte(out), 0644)
// if err != nil {
// m.modLog.Error(err.Error())
// m.CommandCounter.IncrError()
// panic(err.Error())
// }
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Send these requests through your favorite interception proxy"))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
return out
}
func (m *ApiGwModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, api := range Items {
m.CommandCounter.IncrTotal()
for _, endpoint := range m.getEndpointsPerAPIGateway(r, api) {
dataReceiver <- endpoint
}
}
}
func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
GetDomainNames, err := sdk.CachedApiGatewayGetDomainNames(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, item := range GetDomainNames {
domain := aws.ToString(item.DomainName)
GetBasePathMappings, err := sdk.CachedApiGatewayGetBasePathMappings(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, item.DomainName)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
break
}
for _, mapping := range GetBasePathMappings {
stage := aws.ToString(mapping.Stage)
basePath := aws.ToString(mapping.BasePath)
if basePath == "(none)" {
basePath = "" // Empty string since '/' is already prepended
}
for _, api := range Items {
if api.Id != nil && aws.ToString(api.Id) == aws.ToString(mapping.RestApiId) {
m.CommandCounter.IncrTotal()
endpoints := m.getEndpointsPerAPIGateway(r, api)
for _, endpoint := range endpoints {
old := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.RestApiId), r, stage)
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if basePath == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, basePath)
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
break
}
}
}
}
}
func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.RestApi) []ApiGateway {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
var gateways []ApiGateway
//var PaginationControl2 *string
awsService := "APIGateway"
var public string
name := aws.ToString(api.Name)
id := aws.ToString(api.Id)
raw_endpoint := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", id, r)
endpointType := *api.EndpointConfiguration
//fmt.Println(endpointType)
if endpointType.Types[0] == "PRIVATE" {
public = "False"
} else {
public = "True"
}
GetStages, err := sdk.CachedApiGatewayGetStages(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return gateways
}
resources, err := sdk.CachedApiGatewayGetResources(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
for _, stage := range GetStages.Item {
stageName := aws.ToString(stage.StageName)
for _, resource := range resources {
if len(resource.ResourceMethods) != 0 {
for method := range resource.ResourceMethods {
// Check if API Key is required for endpoint
apiKey := ""
if m.ApiGatewayApiKeyRequired(r, api.Id, resource.Id, method) {
apiKey, err = m.GetApiGatewayApiKey(r, id, stageName)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
}
path := aws.ToString(resource.Path)
endpoint := fmt.Sprintf("%s/%s%s", raw_endpoint, stageName, path)
gateways = append(gateways, ApiGateway{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Method: method,
Public: public,
ApiKey: apiKey,
})
}
}
}
}
return gateways
}
func (m *ApiGwModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, api := range Items {
m.CommandCounter.IncrTotal()
for _, endpoint := range m.getEndpointsPerAPIGatewayv2(r, api) {
dataReceiver <- endpoint
}
}
}
func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
GetDomainNames, err := sdk.CachedAPIGatewayv2GetDomainNames(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
for _, item := range GetDomainNames {
domain := aws.ToString(item.DomainName)
GetApiMappings, err := sdk.CachedAPIGatewayv2GetApiMappings(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, domain)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
break
}
for _, mapping := range GetApiMappings {
stage := aws.ToString(mapping.Stage)
if stage == "$default" {
stage = ""
}
path := aws.ToString(mapping.ApiMappingKey)
for _, api := range Items {
if api.ApiId != nil && aws.ToString(api.ApiId) == aws.ToString(mapping.ApiId) {
m.CommandCounter.IncrTotal()
endpoints := m.getEndpointsPerAPIGatewayv2(r, api)
for _, endpoint := range endpoints {
var old string
if stage == "" {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/", aws.ToString(mapping.ApiId), r)
} else {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.ApiId), r, stage)
}
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if path == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, path)
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
break
}
}
}
}
}
func (m *ApiGwModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2Types.Api) []ApiGateway {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
var gateways []ApiGateway
awsService := "APIGatewayv2"
var public string
name := aws.ToString(api.Name)
raw_endpoint := aws.ToString(api.ApiEndpoint)
id := aws.ToString(api.ApiId)
var stages []string
GetStages, err := sdk.CachedAPIGatewayv2GetStages(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
for _, stage := range GetStages {
s := aws.ToString(stage.StageName)
if s == "$default" {
s = ""
}
stages = append(stages, s)
}
GetRoutes, err := sdk.CachedAPIGatewayv2GetRoutes(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
for _, stage := range stages {
for _, route := range GetRoutes {
routeKey := route.RouteKey
var method string
var path string
if len(strings.Fields(*routeKey)) == 2 {
method = strings.Fields(*routeKey)[0]
path = strings.Fields(*routeKey)[1]
}
var endpoint string
if stage == "" {
endpoint = fmt.Sprintf("%s%s", raw_endpoint, path)
} else {
endpoint = fmt.Sprintf("%s/%s%s", raw_endpoint, stage, path)
}
public = "True"
gateways = append(gateways, ApiGateway{
AWSService: awsService,
Region: r,
Name: name,
Method: method,
Endpoint: endpoint,
Public: public,
})
}
}
return gateways
}
func (m *ApiGwModule) ApiGatewayApiKeyRequired(r string, ApiId *string, ResourceId *string, method string) bool {
GetMethod, err := sdk.CachedApiGatewayGetMethod(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, aws.ToString(ApiId), aws.ToString(ResourceId), method)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
} else {
return aws.ToBool(GetMethod.ApiKeyRequired)
}
return false
}
func (m *ApiGwModule) GetApiGatewayApiKey(r string, ApiId string, Stage string) (string, error) {
var items []apigatewayTypes.UsagePlan
GetUsagePlans, err := sdk.CachedApiGatewayGetUsagePlans(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
return "", err
}
for _, item := range GetUsagePlans {
items = append(items, item)
}
for _, item := range items {
for _, apiStage := range item.ApiStages {
if aws.ToString(apiStage.ApiId) == ApiId && aws.ToString(apiStage.Stage) == Stage {
GetUsagePlanKeys, err := sdk.CachedApiGatewayGetUsagePlanKeys(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, aws.ToString(item.Id))
if err != nil {
return "", err
}
for _, i := range GetUsagePlanKeys {
if aws.ToString(i.Type) == "API_KEY" {
return aws.ToString(i.Value), nil
}
}
}
}
}
return "", nil
}
+67
View File
@@ -0,0 +1,67 @@
package aws
import (
"path/filepath"
"strings"
"testing"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/smithy-go/ptr"
"github.com/bishopfox/awsservicemap"
"github.com/spf13/afero"
)
func TestApiGw(t *testing.T) {
// Create a service map that downloads from AWS
serviceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
m := ApiGwModule{
AWSProfile: "unittesting",
AWSRegions: []string{"us-east-1"},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
Account: aws.String("123456789012"),
},
Goroutines: 3,
WrapTable: false,
APIGatewayClient: &sdk.MockedAWSAPIGatewayClient{},
APIGatewayv2Client: &sdk.MockedAWSAPIGatewayv2Client{},
ServiceMap: serviceMap,
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
//tmpDir := "~/.cloudfox/"
tmpDir := ptr.ToString(internal.GetLogDirPath())
m.PrintApiGws(tmpDir, 2)
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/api-gw.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
//print the results file to the screen
//fmt.Println(string(resultsFile))
// I want a test that runs the main function and checks the output to see if the following items are in the output: "https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1", "https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2"
expectedResults := []string{
"https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1",
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2",
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage2/route1",
"23oieuwefo3rfs",
}
for _, expected := range expectedResults {
if !strings.Contains(string(resultsFile), expected) {
t.Errorf("Expected %s to be in the results file", expected)
}
}
}
+200 -61
View File
@@ -1,7 +1,6 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -10,47 +9,54 @@ import (
"sync"
"unicode/utf8"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type S3ListBucketsAPI interface {
ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error)
}
type BucketsModule struct {
// General configuration data
S3Client *s3.Client
//BucketsS3Client CloudFoxS3Client
CheckBucketPolicies bool
S3Client sdk.AWSS3ClientInterface
AWSRegions []string
AWSProfile string
Caller sts.GetCallerIdentityOutput
AWSTableCols string
AWSOutputType string
// This interface is used for unit testing
S3ClientListBucketsInterface S3ListBucketsAPI
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Goroutines int
WrapTable bool
// Main module data
Buckets []Bucket
Buckets []BucketRow
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Bucket struct {
AWSService string
Region string
Name string
type BucketRow struct {
Arn string
AWSService string
Region string
Name string
Policy policy.Policy
PolicyJSON string
Access string
IsPublic string
IsConditionallyPublic string
Statement string
Actions string
ConditionText string
ResourcePolicySummary string
}
func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "buckets"
@@ -61,6 +67,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s][%s] Enumerating buckets for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
@@ -73,7 +80,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan Bucket)
dataReceiver := make(chan BucketRow)
// Create a channel to signal to stop
receiverDone := make(chan bool)
@@ -81,7 +88,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
go m.Receiver(dataReceiver, receiverDone)
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(wg, semaphore, dataReceiver)
wg.Wait()
@@ -94,34 +101,93 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
<-receiverDone
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{"Service", "Region", "Name"}
m.output.Headers = []string{
"Account",
"Name",
"Region",
"Public?",
"Resource Policy Summary",
}
// Table rows
for i := range m.Buckets {
m.output.Body = append(
m.output.Body,
[]string{
m.Buckets[i].AWSService,
m.Buckets[i].Region,
aws.ToString(m.Caller.Account),
m.Buckets[i].Name,
m.Buckets[i].Region,
m.Buckets[i].IsPublic,
m.Buckets[i].ResourcePolicySummary,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
////m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Name",
"Region",
"Public?",
"Resource Policy Summary",
}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{
"Name",
"Region",
"Public?",
"Resource Policy Summary",
}
}
// Remove the Public? and Resource Policy Summary columns if the user did not specify CheckBucketPolicies
if !m.CheckBucketPolicies {
tableCols = removeStringFromSlice(tableCols, "Public?")
tableCols = removeStringFromSlice(tableCols, "Resource Policy Summary")
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
TableCols: tableCols,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
fmt.Printf("[%s][%s] %s buckets found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
fmt.Printf("[%s][%s] Bucket policies written to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
} else {
fmt.Printf("[%s][%s] No buckets found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) {
func (m *BucketsModule) Receiver(receiver chan BucketRow, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
@@ -134,14 +200,14 @@ func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) {
}
}
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
defer wg.Done()
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.createBucketsRows(m.output.Verbosity, wg, semaphore, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile string) {
@@ -188,10 +254,10 @@ func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile
}
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -199,39 +265,112 @@ func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, sem
defer func() {
<-semaphore
}()
var r string = "Global"
var region string = "Global"
var name string
ListBuckets, err := m.listBuckets()
ListBuckets, err := sdk.CachedListBuckets(m.S3Client, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, bucket := range ListBuckets {
name = aws.ToString(bucket.Name)
// Send Bucket object through the channel to the receiver
dataReceiver <- Bucket{
for _, b := range ListBuckets {
bucket := &BucketRow{
Name: aws.ToString(b.Name),
AWSService: "S3",
Name: name,
Region: r,
}
region, err = sdk.CachedGetBucketLocation(m.S3Client, aws.ToString(m.Caller.Account), aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
region = "Unknown"
}
bucket.Region = region
if m.CheckBucketPolicies {
policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
} else {
bucket.PolicyJSON = policyJSON
}
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
} else {
bucket.Policy = policy
}
bucket.IsPublic = "No"
if !bucket.Policy.IsEmpty() {
m.analyseBucketPolicy(bucket, dataReceiver)
} else {
bucket.Access = "No resource policy"
dataReceiver <- *bucket
}
} else {
// Send Bucket object through the channel to the receiver
bucket.Access = "Skipped"
bucket.IsPublic = "Skipped"
dataReceiver <- *bucket
}
}
}
func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool {
publicAccessBlock, err := sdk.CachedGetPublicAccessBlock(m.S3Client, aws.ToString(m.Caller.Account), r, bucketName)
if err != nil {
return false
}
return aws.ToBool(publicAccessBlock.IgnorePublicAcls) && aws.ToBool(publicAccessBlock.BlockPublicPolicy) && aws.ToBool(publicAccessBlock.RestrictPublicBuckets)
}
func (m *BucketsModule) listBuckets() ([]types.Bucket, error) {
func (m *BucketsModule) analyseBucketPolicy(bucket *BucketRow, dataReceiver chan BucketRow) {
m.storeAccessPolicy(bucket)
var buckets []types.Bucket
ListBuckets, err := m.S3ClientListBucketsInterface.ListBuckets(
context.TODO(),
&s3.ListBucketsInput{},
)
if err != nil {
m.modLog.Error(err.Error())
return buckets, err
if bucket.Policy.IsPublic() && !bucket.Policy.IsConditionallyPublic() && !m.isPublicAccessBlocked(bucket.Name, bucket.Region) {
bucket.IsPublic = "YES"
}
buckets = append(buckets, ListBuckets.Buckets...)
return buckets, nil
for i, statement := range bucket.Policy.Statement {
var prefix string = ""
if len(bucket.Policy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
bucket.ResourcePolicySummary = bucket.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
} else {
bucket.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
//bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n")
}
dataReceiver <- *bucket
}
func (m *BucketsModule) storeAccessPolicy(bucket *BucketRow) {
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", bucket.Name))
if err := m.storeFile(f, bucket.PolicyJSON); err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
}
func (m *BucketsModule) getLootDir() string {
return filepath.Join(m.output.FilePath, "loot", "bucket-policies")
}
func (m *BucketsModule) storeFile(filename string, policy string) error {
err := os.MkdirAll(filepath.Dir(filename), 0750)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("creating parent dirs: %s", err)
}
return os.WriteFile(filename, []byte(policy), 0644)
}
+84 -15
View File
@@ -3,6 +3,8 @@ package aws
import (
"context"
"log"
"path/filepath"
"strings"
"testing"
"time"
@@ -11,13 +13,13 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/spf13/afero"
)
type MockedS3ListBuckets struct {
AWSRegions []string
type MockedS3Client struct {
}
func (m *MockedS3ListBuckets) ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
func (m *MockedS3Client) ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
buckets := make([]types.Bucket, 1)
mockBucket := types.Bucket{
CreationDate: aws.Time(time.Now()),
@@ -31,37 +33,104 @@ func (m *MockedS3ListBuckets) ListBuckets(ctx context.Context, params *s3.ListBu
}
func (m *MockedS3Client) GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) {
bucketPolicy := `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddPerm",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::examplebucket/*"
}
]
}`
output := &s3.GetBucketPolicyOutput{
Policy: aws.String(bucketPolicy),
}
return output, nil
}
func (m *MockedS3Client) GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) {
locationConstraint := types.BucketLocationConstraint("us-east-2")
output := &s3.GetBucketLocationOutput{
LocationConstraint: locationConstraint,
}
return output, nil
}
func (m *MockedS3Client) GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) {
output := &s3.GetPublicAccessBlockOutput{
PublicAccessBlockConfiguration: &types.PublicAccessBlockConfiguration{
BlockPublicAcls: aws.Bool(true),
BlockPublicPolicy: aws.Bool(true),
IgnorePublicAcls: aws.Bool(false),
RestrictPublicBuckets: aws.Bool(true),
},
}
return output, nil
}
func TestListBuckets(t *testing.T) {
m := BucketsModule{
S3Client: &MockedS3Client{},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
AWSProfile: "unittesting",
AWSOutputType: "",
CheckBucketPolicies: true,
Goroutines: 3,
}
subtests := []struct {
name string
outputDirectory string
verbosity int
testModule BucketsModule
expectedResult []Bucket
expectedResult []BucketRow
}{
{
name: "test1",
outputDirectory: ".",
verbosity: 2,
testModule: BucketsModule{
S3ClientListBucketsInterface: &MockedS3ListBuckets{},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
},
expectedResult: []Bucket{{
testModule: m,
expectedResult: []BucketRow{{
Name: "mockBucket123",
}},
},
}
internal.MockFileSystem(true)
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := "."
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintBuckets(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
subtest.testModule.PrintBuckets(subtest.outputDirectory, subtest.verbosity)
for index, expectedBucket := range subtest.expectedResult {
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/buckets.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
expectedResults := strings.TrimLeft(`
╭───────────────┬───────────┬─────────┬───────────────────────────╮
│ Name │ Region │ Public? │ Resource Policy Summary │
├───────────────┼───────────┼─────────┼───────────────────────────┤
│ mockBucket123 │ us-east-2 │ YES │ Everyone can s3:GetObject │
╰───────────────┴───────────┴─────────┴───────────────────────────╯
`, "\n")
if string(resultsFile) != expectedResults {
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
}
if expectedBucket.Name != subtest.testModule.Buckets[index].Name {
log.Fatal("Bucket name does not match expected name")
}
+538
View File
@@ -0,0 +1,538 @@
package aws
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.Copy().BorderStyle(b)
}()
magentaStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("magenta"))
)
type model struct {
preloadedData *AllAccountData
awsAccountsTable table.Model
awsAccountsViewport viewport.Model
mainTable table.Model
mainTableViewport viewport.Model
mainTableData map[int][]table.Row
detailsData map[int]string
detailsViewport viewport.Model // Use viewport for details view
focusSelector int
defaultTableStyle table.Styles
terminalWidth int
terminalHeight int // Add terminal height to manage viewport size
awsSelectedRow int
mainSelectedRow int // Track the currently selected row for detail view updates
keys keyMap
help help.Model
lastKey string
quitting bool
}
type CapeJSON struct {
Account string `json:"account"`
Source string `json:"source"`
Summary string `json:"summary"`
Target string `json:"target"`
IsTargetAdmin string `json:"isTargetAdmin"`
}
type PerAccountData struct {
FilePath string // Path to the JSON file
PrivescPaths []CapeJSON // All records contained in the file
}
type AllAccountData struct {
Files map[string]*PerAccountData // Map of file paths to their records
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.terminalWidth = msg.Width
m.terminalHeight = msg.Height - 7 // Adjust as needed
// Calculate heights based on the specified percentages
awsAccountsHeight := int(float32(m.terminalHeight) * 0.2)
halfHeight := int(float32(m.terminalHeight) * 0.4) // For the other two viewports
// Update dimensions for all viewports
m.awsAccountsViewport.Width = m.terminalWidth - 4
m.awsAccountsViewport.Height = awsAccountsHeight
m.mainTableViewport.Width = m.terminalWidth - 4
m.mainTableViewport.Height = halfHeight
m.awsAccountsTable.SetHeight(awsAccountsHeight - 2)
m.awsAccountsTable.SetWidth(m.terminalWidth - 4)
m.awsAccountsViewport.Width = m.terminalWidth - 4
m.detailsViewport.Width = m.terminalWidth - 4
m.detailsViewport.Height = halfHeight
m.mainTable.SetHeight(halfHeight - 2)
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keys.Up):
m.lastKey = "↑"
case key.Matches(msg, m.keys.Down):
m.lastKey = "↓"
case key.Matches(msg, m.keys.Left):
m.lastKey = "←"
case key.Matches(msg, m.keys.Right):
m.lastKey = "→"
case key.Matches(msg, m.keys.Help):
m.help.ShowAll = !m.help.ShowAll
case key.Matches(msg, m.keys.Tab):
// Switch focus between the the three viewports by cycling focusSelector between 0, 1, and 2
m.focusSelector = (m.focusSelector + 1) % 3
//m.focusOnTable = !m.focusOnTable
// add a case for shift and tab at the same time to to cycle the focusSelector backwards between 0, 1, and 2
case key.Matches(msg, m.keys.ShiftTab):
m.focusSelector = (m.focusSelector - 1) % 3
case key.Matches(msg, m.keys.Quit):
m.quitting = true
return m, tea.Quit
}
//var detailsData map[int]string
//var mainTable table.Model
var err error
var awsCurrentRow int
var mainCurrentRow int
switch m.focusSelector {
case 0:
m.awsAccountsTable, cmd = m.awsAccountsTable.Update(msg)
m.awsAccountsViewport.SetContent(m.awsAccountsTable.View())
awsCurrentRow = m.awsAccountsTable.Cursor()
if m.awsSelectedRow != awsCurrentRow {
m.awsSelectedRow = awsCurrentRow
// Update the viewport content based on the newly selected row
// Load the file for the selected account and show the file content in the main table
m.mainTable, m.detailsData, err = getRecordsForAccount(m.preloadedData.Files[m.awsAccountsTable.Rows()[awsCurrentRow][0]])
if err != nil {
// Handle error
break
}
m.mainTable.SetStyles(s)
m.mainTableViewport.SetContent(m.mainTable.View())
m.detailsViewport.SetContent(m.detailsData[0])
}
case 1:
m.mainTable, cmd = m.mainTable.Update(msg)
// Check if the selected row has changed
m.mainTableViewport.SetContent(m.mainTable.View())
mainCurrentRow = m.mainTable.Cursor()
if m.mainSelectedRow != mainCurrentRow {
m.mainSelectedRow = mainCurrentRow
// Update the viewport content based on the newly selected row
if detail, ok := m.detailsData[m.mainSelectedRow]; ok {
m.detailsViewport.SetContent(detail)
} else {
m.detailsViewport.SetContent(fmt.Sprintf("No details available for main row %d (aws row %d)", m.mainSelectedRow, m.awsSelectedRow))
}
}
case 2:
m.detailsViewport, cmd = m.detailsViewport.Update(msg)
}
}
return m, cmd
}
func (m model) View() string {
// Define styles for active and inactive viewports
activeBorderStyle := lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#00FFFF")). // Cyan for active
Padding(0)
activeBorderStyle.PaddingLeft(1)
activeBorderStyle.PaddingRight(1)
inactiveBorderStyle := lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#808080")). // Gray for inactive
Padding(0)
inactiveBorderStyle.PaddingLeft(1)
inactiveBorderStyle.PaddingRight(1)
// Conditionally apply border styles based on focus
awsTableViewStyle := activeBorderStyle
m.awsAccountsTable.SetStyles(m.defaultTableStyle)
tableViewStyle := inactiveBorderStyle
m.mainTable.SetStyles(m.defaultTableStyle)
detailsViewStyle := inactiveBorderStyle
switch m.focusSelector {
case 0:
awsTableViewStyle = activeBorderStyle
m.awsAccountsTable.SetStyles(m.defaultTableStyle)
tableViewStyle = inactiveBorderStyle
detailsViewStyle = inactiveBorderStyle
case 1:
awsTableViewStyle = inactiveBorderStyle
tableViewStyle = activeBorderStyle
m.mainTable.SetStyles(m.defaultTableStyle)
detailsViewStyle = inactiveBorderStyle
case 2:
awsTableViewStyle = inactiveBorderStyle
tableViewStyle = inactiveBorderStyle
detailsViewStyle = activeBorderStyle
}
// Render the table and details viewports with their styles (adjust based on focus)
awsAccountsView := awsTableViewStyle.Render(m.awsAccountsViewport.View())
tableView := tableViewStyle.Render(m.mainTableViewport.View())
detailsView := detailsViewStyle.Render(m.detailsViewport.View())
// Combine all views
fullView := lipgloss.JoinVertical(lipgloss.Top, awsAccountsView, tableView, detailsView)
helpView := m.help.View(m.keys)
return fmt.Sprintf("%s\n%s", fullView, helpView)
}
func calculateMaxWidths(rows []table.Row) []int {
var maxWidths []int
// make sure the length of the rows is greater than 0
if len(rows) > 0 {
maxWidths = make([]int, len(rows[0]))
} else {
maxWidths = []int{30, 30, 30, 30}
}
for _, row := range rows {
for i, cell := range row {
if len(cell) > maxWidths[i] {
maxWidths[i] = len(cell)
}
}
}
return maxWidths
}
func (m model) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.detailsViewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.detailsViewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func loadFileRecords(filePath string) (*PerAccountData, error) {
// Open the file
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// Decode the JSON data
var records []CapeJSON
decoder := json.NewDecoder(file)
if err := decoder.Decode(&records); err != nil {
return nil, err
}
// Return a new FileRecords instance
return &PerAccountData{
FilePath: filePath,
PrivescPaths: records,
}, nil
}
func preloadData(filePaths []string) (*AllAccountData, error) {
appData := &AllAccountData{
Files: make(map[string]*PerAccountData),
}
for _, filePath := range filePaths {
fileRecords, err := loadFileRecords(filePath)
if err != nil {
return nil, err
}
appData.Files[filePath] = fileRecords
}
return appData, nil
}
func getRecordsForAccount(preloadedData *PerAccountData) (table.Model, map[int]string, error) {
// lets load the records for the first file in the list
records := preloadedData.PrivescPaths
if len(records) < 1 {
mainTable := table.New(
table.WithColumns([]table.Column{
{Title: "Account", Width: 30},
{Title: "Source", Width: 30},
{Title: "Target", Width: 30},
{Title: "isTargetAdmin", Width: 30},
}),
table.WithRows([]table.Row{{"No records found", "", "", ""}}),
table.WithFocused(true),
)
return mainTable, nil, nil
}
// Prepare rows for the table and data for the right view
rows := make([]table.Row, 0, len(records)-1)
detailsData := make(map[int]string) // Initialize the map for the fourth column's data
for i, record := range records[1:] { // Skip the header row
rows = append(rows, table.Row{record.Account, record.Source, record.Target, record.IsTargetAdmin})
detailsData[i] = expandDetailsData(record.Summary)
}
// Calculate max widths
maxWidths := calculateMaxWidths(rows)
// Define columns with calculated widths
columns := make([]table.Column, len(maxWidths))
colNames := []string{}
for _, header := range []string{"Account", "Source", "Target", "isTargetAdmin"} {
colNames = append(colNames, header)
}
for i, width := range maxWidths {
colName := colNames[i]
// if column name width is greater than the calculated width, use the column name width
if len(colName) > width {
width = len(colName)
}
columns[i] = table.Column{
Title: colName,
Width: width,
}
}
mainTable := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
)
return mainTable, detailsData, nil
}
func CapeTUI(outputFiles []string) {
preloadData, err := preloadData(outputFiles)
if err != nil {
fmt.Printf("Error preloading data: %s\n", err)
fmt.Println("Either remove this profile from the list of profiles, or make sure cape can run successfully for this profile")
os.Exit(1)
}
var awsAccountsRows []table.Row
for _, file := range preloadData.Files {
// Extract a user-friendly account name from the file path if needed
// For simplicity, here we use the file path itself
awsAccountsRows = append(awsAccountsRows, table.Row{file.FilePath})
}
mainTable, detailsData, err := getRecordsForAccount(preloadData.Files[outputFiles[0]])
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
awsAccountsWidth := calculateMaxWidths(awsAccountsRows)
awsAccountsTable := table.New(
table.WithColumns([]table.Column{
{Title: "AWS Accounts", Width: awsAccountsWidth[0]},
}),
table.WithRows(awsAccountsRows),
table.WithFocused(true), // Initially unfocused
)
awsAccountsTable.SetStyles(s)
awsAccountsViewport := viewport.New(0, 0) // Initialize with 0 size; will be updated
awsAccountsViewport.SetContent(awsAccountsTable.View()) // Set initial content
mainTable.SetStyles(s)
mainTableViewport := viewport.New(0, 0) // Initialize with 0 size; it will be updated
mainTableViewport.SetContent(mainTable.View()) // Set the initial content of the table viewport
// Initialize viewport for details view
detailsViewportModel := viewport.New(0, 0) // Size will be set based on terminal size in Update
// show the first row's details by default
detailsViewportModel.SetContent(detailsData[0])
m := model{
preloadedData: preloadData,
awsAccountsTable: awsAccountsTable,
awsAccountsViewport: awsAccountsViewport,
mainTable: mainTable,
mainTableViewport: mainTableViewport,
detailsData: detailsData,
detailsViewport: detailsViewportModel,
//focusOnTable: true,
focusSelector: 0,
mainSelectedRow: 0, // Initialize selectedRow with an invalid index
awsSelectedRow: 0,
defaultTableStyle: s,
keys: keys,
help: help.New(),
}
p := tea.NewProgram(m)
if err := p.Start(); err != nil {
fmt.Printf("Error starting program: %s\n", err)
os.Exit(1)
}
}
// keyMap defines a set of keybindings. To work for help it must satisfy
// key.Map. It could also very easily be a map[string]key.Binding.
type keyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Help key.Binding
Quit key.Binding
Tab key.Binding
ShiftTab key.Binding
}
// ShortHelp returns keybindings to be shown in the mini help view. It's part
// of the key.Map interface.
func (k keyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Tab, k.ShiftTab, k.Up, k.Down, k.Help, k.Quit}
}
// FullHelp returns keybindings for the expanded help view. It's part of the
// key.Map interface.
func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Up, k.Down, k.Left, k.Right}, // first column
{k.Tab, k.ShiftTab, k.Help, k.Quit}, // second column
}
}
var keys = keyMap{
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "move up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "move down"),
),
Left: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("←/h", "move left"),
),
Right: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("→/l", "move right"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "toggle help"),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("q", "quit"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "Switch window focus"),
),
ShiftTab: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("shift+tab", "Switch window focus"),
),
}
func expandDetailsData(input string) string {
output := ""
lines := strings.Split(input, "\n")
for _, line := range lines {
var hop, option int
var rest string
// Use Sscanf to extract hop and option values
n, err := fmt.Sscanf(line, "[Hop: %d] [Option: %d]", &hop, &option)
if err != nil || n != 2 {
fmt.Printf("Error parsing line, expected 2 got %d: %v\n", n, err)
continue
}
// Extract the rest of the line manually
restIndex := strings.Index(line, "]") + 1
restIndex = strings.Index(line[restIndex:], "]") + restIndex + 1
if restIndex > 1 && restIndex < len(line) {
rest = line[restIndex+1:] // +1 to skip the space after the second ]
}
// Construct the formatted output
formattedLine := fmt.Sprintf("[Hop: %d][Option: %d]\n\t%s\n\n", hop, option, rest)
output += formattedLine
}
return output
}
+1049
View File
File diff suppressed because it is too large. Load diff
+161
View File
@@ -0,0 +1,161 @@
package aws
import (
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codebuild"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/efs"
"github.com/aws/aws-sdk-go-v2/service/fsx"
"github.com/aws/aws-sdk-go-v2/service/glue"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/lambda"
"github.com/aws/aws-sdk-go-v2/service/organizations"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
func InitIamCommandClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
iamSimMod := IamSimulatorModule{
IAMClient: iamSimPPClient,
Caller: caller,
AWSProfileProvided: AWSProfile,
Goroutines: Goroutines,
modLog: internal.TxtLog.WithFields(logrus.Fields{"module": "iam-simulator"}),
}
return iamSimMod
}
func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool, AWSMFAToken string) SNSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
cloudFoxSNSClient := SNSModule{
SNSClient: sns.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
}
return cloudFoxSNSClient
}
func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, AWSMFAToken string) BucketsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
cloudFoxS3Client := BucketsModule{
S3Client: s3.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return cloudFoxS3Client
}
func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) SQSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
sqsClient := SQSModule{
SQSClient: sqs.NewFromConfig(AWSConfig),
Caller: caller,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSProfile: AWSProfile,
Goroutines: Goroutines,
}
return sqsClient
}
func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) LambdasModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
lambdaClient := LambdasModule{
LambdaClient: lambda.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return lambdaClient
}
func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) CodeBuildModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
codeBuildClient := CodeBuildModule{
CodeBuildClient: codebuild.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return codeBuildClient
}
func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) ECRModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
ecrClient := ECRModule{
ECRClient: ecr.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return ecrClient
}
func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) FilesystemsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
fileSystemsClient := FilesystemsModule{
EFSClient: efs.NewFromConfig(AWSConfig),
FSxClient: fsx.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return fileSystemsClient
}
func InitOrgsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) OrgModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
orgClient := OrgModule{
OrganizationsClient: organizations.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return orgClient
}
func InitPermissionsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) IamPermissionsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
permissionsClient := IamPermissionsModule{
IAMClient: iam.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return permissionsClient
}
func InitSecretsManagerClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) *secretsmanager.Client {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
return secretsmanager.NewFromConfig(AWSConfig)
}
func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) *glue.Client {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
return glue.NewFromConfig(AWSConfig)
}
func InitOrgClient(AWSConfig aws.Config) *organizations.Client {
return organizations.NewFromConfig(AWSConfig)
}
func InitIAMClient(AWSConfig aws.Config) *iam.Client {
return iam.NewFromConfig(AWSConfig)
}
+96 -106
View File
@@ -1,36 +1,34 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type CloudFormationGetTemplateAPIClient interface {
GetTemplate(context.Context, *cloudformation.GetTemplateInput, ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error)
}
type CloudformationModule struct {
// General configuration data
CloudFormationDescribeStacksInterface cloudformation.DescribeStacksAPIClient
CloudFormationGetTemplateInterface CloudFormationGetTemplateAPIClient
CloudFormationClient sdk.CloudFormationClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
AWSProfile string
WrapTable bool
AWSOutputType string
AWSTableCols string
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
CFStacks []CFStack
@@ -50,8 +48,8 @@ type CFStack struct {
Template string
}
func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "cloudformation"
@@ -82,7 +80,7 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -98,48 +96,94 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
"Role",
// "Parameters",
// "Outputs",
"Parameters",
"Outputs",
}
// Table rows
for i := range m.CFStacks {
// var isParameters string
// var isOutputs string
// if m.CFStacks[i].Parameters != nil {
// isParameters = "Y"
// } else {
// isParameters = "N"
// }
// if m.CFStacks[i].Outputs != nil {
// isOutputs = "Y"
// } else {
// isOutputs = "N"
// }
var hasParameters string
var hasOutputs string
if m.CFStacks[i].Parameters != nil {
hasParameters = "Y"
} else {
hasParameters = "N"
}
if m.CFStacks[i].Outputs != nil {
hasOutputs = "Y"
} else {
hasOutputs = "N"
}
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.CFStacks[i].AWSService,
m.CFStacks[i].Region,
m.CFStacks[i].Name,
m.CFStacks[i].Role,
// isParameters,
// isOutputs,
hasParameters,
hasOutputs,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Region",
"Name",
"Role",
"Parameters",
"Outputs",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"Role",
}
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s cloudformation stacks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No cloudformation stacks found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -149,8 +193,12 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou
func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
serviceRegions, err := servicemap.GetRegionsForService("cloudformation")
if err != nil {
@@ -158,7 +206,7 @@ func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semap
}
for _, serviceRegion := range serviceRegions {
if r == serviceRegion {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.createCFStackRowsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -183,7 +231,7 @@ func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int)
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "cloudformation-data.txt")
@@ -223,7 +271,7 @@ func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int)
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -240,8 +288,8 @@ func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int)
func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -251,10 +299,10 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
}()
var stackTemplateBody string = ""
DescribeStacks, err := m.describeStacks(r)
DescribeStacks, err := sdk.CachedCloudFormationDescribeStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stack := range DescribeStacks {
@@ -263,10 +311,10 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
stackOutputs := stack.Outputs
stackParameters := stack.Parameters
stackTemplateBody, err = m.getTemplateBody(r, stackName)
stackTemplateBody, err = sdk.CachedCloudFormationGetTemplate(m.CloudFormationClient, aws.ToString(m.Caller.Account), r, stackName)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
dataReceiver <- CFStack{
@@ -281,61 +329,3 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
}
}
func (m *CloudformationModule) describeStacks(r string) ([]types.Stack, error) {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var stacks []types.Stack
for {
DescribeStacks, err := m.CloudFormationDescribeStacksInterface.DescribeStacks(
context.TODO(),
&cloudformation.DescribeStacksInput{
NextToken: PaginationControl,
},
func(o *cloudformation.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return stacks, err
}
stacks = append(stacks, DescribeStacks.Stacks...)
// The "NextToken" value is nil when there's no more data to return.
if DescribeStacks.NextToken != nil {
PaginationControl = DescribeStacks.NextToken
} else {
PaginationControl = nil
break
}
}
return stacks, nil
}
func (m *CloudformationModule) getTemplateBody(r string, stackName string) (string, error) {
var stackTemplateBody string
GetTemplate, err := m.CloudFormationGetTemplateInterface.GetTemplate(
context.TODO(),
&cloudformation.GetTemplateInput{
StackName: &stackName,
},
func(o *cloudformation.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return stackTemplateBody, err
}
stackTemplateBody = aws.ToString(GetTemplate.TemplateBody)
return stackTemplateBody, nil
}
+7 -74
View File
@@ -1,80 +1,15 @@
package aws
import (
"context"
"encoding/json"
"log"
"testing"
"time"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json"
const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json"
type Stacks struct {
Stacks []struct {
StackID string `json:"StackId"`
Description string `json:"Description"`
Tags []interface{} `json:"Tags"`
Outputs []types.Output `json:"Outputs"`
Parameters []types.Parameter `json:"Parameters"`
StackStatusReason interface{} `json:"StackStatusReason"`
CreationTime time.Time `json:"CreationTime"`
Capabilities []interface{} `json:"Capabilities"`
StackName string `json:"StackName"`
RoleArn string `json:"RoleArn"`
StackStatus string `json:"StackStatus"`
DisableRollback bool `json:"DisableRollback"`
} `json:"Stacks"`
}
type TemplateBody struct {
TemplateBody string `json:"TemplateBody"`
}
type MockedCloudformationClientDescribeStacks struct {
describeStacks Stacks
}
type MockedCloudformationClientGetTemplate struct {
getTemplateBody TemplateBody
}
func (m *MockedCloudformationClientDescribeStacks) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE)
}
var stacks []types.Stack
for _, stack := range m.describeStacks.Stacks {
stacks = append(stacks, types.Stack{
StackName: &stack.StackName,
RoleARN: &stack.RoleArn,
Outputs: stack.Outputs,
Parameters: stack.Parameters,
})
}
return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil
}
func (m *MockedCloudformationClientGetTemplate) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody)
if err != nil {
log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE)
}
return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil
}
func TestCloudFormation(t *testing.T) {
subtests := []struct {
name string
@@ -88,13 +23,11 @@ func TestCloudFormation(t *testing.T) {
outputDirectory: ".",
verbosity: 2,
testModule: CloudformationModule{
CloudFormationDescribeStacksInterface: &MockedCloudformationClientDescribeStacks{},
CloudFormationGetTemplateInterface: &MockedCloudformationClientGetTemplate{},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
CloudFormationClient: &sdk.MockedCloudformationClient{},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
},
expectedResult: []CFStack{{
Name: "myteststack",
@@ -105,7 +38,7 @@ func TestCloudFormation(t *testing.T) {
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintCloudformationStacks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
subtest.testModule.PrintCloudformationStacks(subtest.outputDirectory, subtest.verbosity)
for index, expectedStack := range subtest.expectedResult {
if expectedStack.Name != subtest.testModule.CFStacks[index].Name {
log.Fatal("Stack name does not match expected name")
+273
View File
@@ -0,0 +1,273 @@
package aws
import (
"fmt"
"path/filepath"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type CodeBuildModule struct {
// General configuration data
CodeBuildClient sdk.CodeBuildClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Main module data
Projects []Project
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Project struct {
Region string
Name string
Arn string
Role string
Admin string
CanPrivEsc string
}
func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "codebuild"
localAdminMap := make(map[string]bool)
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s][%s] Enumerating CodeBuild projects for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Project)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
wg.Wait()
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// Perform role analysis
if m.pmapperError == nil {
for i := range m.Projects {
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Projects[i].Role)
}
} else {
for i := range m.Projects {
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Projects[i].Role, m.iamSimClient, localAdminMap)
}
}
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
// Table rows
for i := range m.Projects {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Projects[i].Region,
m.Projects[i].Name,
m.Projects[i].Role,
m.Projects[i].Admin,
m.Projects[i].CanPrivEsc,
},
)
}
var seen []string
for _, project := range m.Projects {
if !internal.Contains(project.Name, seen) {
seen = append(seen, project.Name)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %d projects found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
} else {
fmt.Printf("[%s][%s] No projects found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *CodeBuildModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
defer wg.Done()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("codebuild", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getcodeBuildProjectsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *CodeBuildModule) Receiver(receiver chan Project, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Projects = append(m.Projects, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *CodeBuildModule) getcodeBuildProjectsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
projects, err := sdk.CachedCodeBuildListProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
}
for _, project := range projects {
details, err := sdk.CachedCodeBuildBatchGetProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r, project)
if err != nil {
sharedLogger.Error(err.Error())
}
dataReceiver <- Project{
Name: aws.ToString(details.Name),
Region: r,
Role: aws.ToString(details.ServiceRole),
Admin: "",
CanPrivEsc: "",
}
}
}
+150
View File
@@ -0,0 +1,150 @@
package aws
import (
"context"
"fmt"
"path/filepath"
"strings"
"testing"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codebuild"
"github.com/aws/aws-sdk-go-v2/service/codebuild/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/spf13/afero"
)
type mockedCodeBuildClient struct {
}
func (m *mockedCodeBuildClient) ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) {
out := codebuild.ListProjectsOutput{
Projects: []string{"test1", "test2"},
}
return &out, nil
}
func (m *mockedCodeBuildClient) BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error) {
project1out := codebuild.BatchGetProjectsOutput{
Projects: []types.Project{
{
Name: aws.String("test1"),
Environment: &types.ProjectEnvironment{
Image: aws.String("aws/codebuild/standard:4.0"),
},
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test1"),
Source: &types.ProjectSource{
Type: "CODECOMMIT",
Location: aws.String("https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test1"),
},
Artifacts: &types.ProjectArtifacts{
Type: "S3",
Location: aws.String("s3://test1"),
},
TimeoutInMinutes: aws.Int32(60),
},
},
}
project2out := codebuild.BatchGetProjectsOutput{
Projects: []types.Project{
{
Name: aws.String("test2"),
Environment: &types.ProjectEnvironment{
Image: aws.String("aws/codebuild/standard:4.0"),
},
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test2"),
Source: &types.ProjectSource{
Type: "GITHUB",
Location: aws.String("https://test.github.com/test2"),
},
Artifacts: &types.ProjectArtifacts{
Type: "S3",
Location: aws.String("s3://test2"),
},
TimeoutInMinutes: aws.Int32(60),
},
},
}
if params.Names[0] == "test1" {
return &project1out, nil
} else if params.Names[0] == "test2" {
return &project2out, nil
} else {
return nil, nil
}
}
func (m *mockedCodeBuildClient) GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) {
out := codebuild.GetResourcePolicyOutput{
Policy: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"codebuild:StartBuild",
"codebuild:BatchGetBuilds",
"codebuild:StopBuild",
"codebuild:ListProjects",
"codebuild:BatchGetProjects"
],
"Resource": "*"
}
]
}`),
}
return &out, nil
}
func TestCodeBuildProjects(t *testing.T) {
m := CodeBuildModule{
CodeBuildClient: &mockedCodeBuildClient{},
AWSProfile: "unittesting",
AWSRegions: []string{"us-west-1"},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
Account: aws.String("123456789012"),
},
Goroutines: 3,
WrapTable: true,
SkipAdminCheck: true,
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := "."
// execute the module with verbosity set to 2
m.PrintCodeBuildProjects(tmpDir, 2)
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/codebuild.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
expectedResults := strings.TrimLeft(`
╭───────────┬───────┬──────────────────────────────────────┬──────────────╮
│ Region │ Name │ Role │ IsAdminRole? │
├───────────┼───────┼──────────────────────────────────────┼──────────────┤
│ us-west-1 │ test1 │ arn:aws:iam::123456789012:role/test1 │ Skipped │
│ us-west-1 │ test2 │ arn:aws:iam::123456789012:role/test2 │ Skipped │
╰───────────┴───────┴──────────────────────────────────────┴──────────────╯
`, "\n")
fmt.Println(expectedResults)
fmt.Println("results file")
fmt.Println(string(resultsFile))
if string(resultsFile) != expectedResults {
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
}
}
+583
View File
@@ -0,0 +1,583 @@
package aws
import (
"errors"
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type DatabasesModule struct {
RDSClient sdk.RDSClientInterface
RedshiftClient sdk.AWSRedShiftClientInterface
DynamoDBClient sdk.DynamoDBClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Databases []Database
CommandCounter internal.CommandCounter
Errors []string
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Database struct {
AWSService string
Region string
Engine string
Name string
Arn string
UserName string
Endpoint string
Port int32
Protocol string
Public string
Size string
Roles string
}
func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "databases"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s][%s] Enumerating databases for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: RDS, Redshift, DynamoDB, DocumentDB, Neptune\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan Database)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
//execute regional checks
for _, region := range m.AWSRegions {
wg.Add(1)
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
wg.Wait()
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
sort.Slice(m.Databases, func(i, j int) bool {
return m.Databases[i].AWSService < m.Databases[j].AWSService
})
m.output.Headers = []string{
"Account",
"Service",
"Engine",
"Region",
"Name",
"Size",
"UserName",
"Endpoint",
"Port",
"Roles",
//"Protocol",
//"Public",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Engine",
"Region",
"Name",
"Size",
"UserName",
"Endpoint",
"Port",
"Roles",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Engine",
"Region",
"Name",
"Size",
"UserName",
"Endpoint",
"Port",
}
}
// Table rows
for i := range m.Databases {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Databases[i].AWSService,
m.Databases[i].Engine,
m.Databases[i].Region,
m.Databases[i].Name,
m.Databases[i].Size,
m.Databases[i].UserName,
m.Databases[i].Endpoint,
strconv.Itoa(int(m.Databases[i].Port)),
m.Databases[i].Roles,
// m.Databases[i].Protocol,
// m.Databases[i].Public,
},
)
}
if len(m.output.Body) > 0 {
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
DirectoryName: filepath,
},
Loot: internal.LootClient{
DirectoryName: filepath,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
loot := m.writeLoot(filepath, verbosity)
//o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.writeLoot(o.Table.DirectoryName, verbosity)
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
Name: "databases-UrlsOnly.txt",
Contents: loot,
})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
fmt.Printf("[%s][%s] %s databases found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No databases found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *DatabasesModule) Receiver(receiver chan Database, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Databases = append(m.Databases, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *DatabasesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer wg.Done()
// Use shared ServiceMap instance if provided, otherwise create a new one
serviceMap := m.ServiceMap
if serviceMap == nil {
serviceMap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
m.executeRdsCheck(r, wg, semaphore, dataReceiver, serviceMap) // Also returns Neptune and DocDB
m.executeRedshiftCheck(r, wg, semaphore, dataReceiver, serviceMap)
m.executeDynamoDbCheck(r, wg, semaphore, dataReceiver, serviceMap)
}
type check struct {
region string
wg *sync.WaitGroup
semaphore chan struct{}
dataReceiver chan Database
serviceMap *awsservicemap.AwsServiceMap
service string
executor func(string, *sync.WaitGroup, chan struct{}, chan Database)
}
func (m *DatabasesModule) executeCheck(check check) {
res, err := check.serviceMap.IsServiceInRegion(check.service, check.region)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.IncrTotal()
check.wg.Add(1)
go check.executor(check.region, check.wg, check.semaphore, check.dataReceiver)
}
}
func (m *DatabasesModule) executeRdsCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
service: "rds",
executor: m.getRdsClustersPerRegion,
})
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
service: "rds",
executor: m.getRdsInstancesPerRegion,
})
}
func (m *DatabasesModule) executeRedshiftCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
service: "redshift",
executor: m.getRedshiftDatabasesPerRegion,
})
}
func (m *DatabasesModule) executeDynamoDbCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
service: "dynamodb",
executor: m.getDynamoDBTablesPerRegion,
})
}
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) string {
path := filepath.Join(outputDirectory, "loot")
f := filepath.Join(path, "databases-UrlsOnly.txt")
var out string
for _, database := range m.Databases {
out = out + fmt.Sprintln(database.Endpoint)
}
// err = os.WriteFile(f, []byte(out), 0644)
// if err != nil {
// m.modLog.Error(err.Error())
// m.CommandCounter.IncrError()
// panic(err.Error())
// }
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed this databases into nmap and something like gowitness/aquatone for screenshots."))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
return out
}
func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
DBClusters, err := sdk.CachedRDSDescribeDBClusters(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, cluster := range DBClusters {
var public string
var service string
var roles string
if cluster.Endpoint == nil {
continue
}
name := aws.ToString(cluster.DBClusterIdentifier)
port := cluster.Port
endpoint := aws.ToString(cluster.Endpoint)
engine := aws.ToString(cluster.Engine)
if aws.ToBool(cluster.PubliclyAccessible) {
public = "True"
} else {
public = "False"
}
if isNeptune(cluster.Engine) {
service = "Neptune"
} else if isDocDB(cluster.Engine) {
service = "DocsDB"
} else {
service = "RDS"
}
associatedRoles := cluster.AssociatedRoles
for _, role := range associatedRoles {
roles = roles + aws.ToString(role.RoleArn) + " "
}
dataReceiver <- Database{
AWSService: service,
Region: r,
Name: name,
Engine: engine,
Endpoint: endpoint,
UserName: aws.ToString(cluster.MasterUsername),
Port: aws.ToInt32(port),
Protocol: aws.ToString(cluster.Engine),
Public: public,
Roles: roles,
}
}
}
func (m *DatabasesModule) getRdsInstancesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, instance := range DBInstances {
var public string
var service string
var roles string
if instance.Endpoint == nil {
continue
}
name := aws.ToString(instance.DBInstanceIdentifier)
port := instance.Endpoint.Port
endpoint := aws.ToString(instance.Endpoint.Address)
engine := aws.ToString(instance.Engine)
if aws.ToBool(instance.PubliclyAccessible) {
public = "True"
} else {
public = "False"
}
if isNeptune(instance.Engine) {
service = "Neptune"
} else if isDocDB(instance.Engine) {
service = "DocsDB"
} else {
service = "RDS"
}
associatedRoles := instance.AssociatedRoles
for _, role := range associatedRoles {
roles = roles + aws.ToString(role.RoleArn) + " "
}
dataReceiver <- Database{
AWSService: service,
Region: r,
Name: name,
Engine: engine,
Endpoint: endpoint,
UserName: aws.ToString(instance.MasterUsername),
Port: aws.ToInt32(port),
Protocol: aws.ToString(instance.Engine),
Public: public,
Roles: roles,
}
}
}
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "Redshift"
protocol := "https"
Clusters, err := sdk.CachedRedShiftDescribeClusters(m.RedshiftClient, aws.ToString(m.Caller.Account), r)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
var public string
for _, cluster := range Clusters {
name := aws.ToString(cluster.DBName)
//id := workspace.Id
endpoint := aws.ToString(cluster.Endpoint.Address)
port := aws.ToInt32(cluster.Endpoint.Port)
if aws.ToBool(cluster.PubliclyAccessible) {
public = "True"
} else {
public = "False"
}
dataReceiver <- Database{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
Protocol: protocol,
Public: public,
}
}
}
func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "DynamoDB"
Tables, err := sdk.CachedDynamoDBListTables(m.DynamoDBClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, table := range Tables {
//get size of dynamodb table
TableOutput, err := sdk.CachedDynamoDBDescribeTable(m.DynamoDBClient, aws.ToString(m.Caller.Account), r, table)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
size := aws.ToInt64(TableOutput.TableSizeBytes)
dataReceiver <- Database{
AWSService: awsService,
Region: r,
Name: table,
Size: strconv.Itoa(int(size)),
UserName: "N/A",
Endpoint: "N/A",
}
}
}
func isNeptune(engine *string) bool {
return *engine == "neptune"
}
func isDocDB(engine *string) bool {
return *engine == "docdb"
}
+58
View File
@@ -0,0 +1,58 @@
package aws
import (
"path/filepath"
"strings"
"testing"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/smithy-go/ptr"
"github.com/spf13/afero"
)
func TestDatabasesCommand(t *testing.T) {
m := DatabasesModule{
AWSProfile: "unittesting",
AWSRegions: []string{"us-east-1"},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
Account: aws.String("123456789012"),
},
Goroutines: 3,
WrapTable: false,
RDSClient: &sdk.MockedRDSClient{},
DynamoDBClient: &sdk.MockedAWSDynamoDBClient{},
RedshiftClient: &sdk.MockedRedshiftClient{},
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := ptr.ToString(internal.GetLogDirPath())
m.PrintDatabases(tmpDir, 2)
//resultsFile, err := afero.ReadFile(fs, "table/databases.txt")
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/databases.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFile, err)
}
expectedResults := []string{
"db1.cluster-123456789012.us-west-2.rds.amazonaws.com", // make sure it includes the Aurora clusters
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com", // make sure it includes the Aurora clusters
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com", // make sure it includes the Neptune instances
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com", // make sure it includes the DocumentDB instances
"db1-instances-1.blah.us-west-2.rds.amazonaws.com", // make sure it includes the RDS instances
}
for _, expected := range expectedResults {
if !strings.Contains(string(resultsFile), expected) {
t.Errorf("Expected %s to be in the results file", expected)
}
}
}
+281
View File
@@ -0,0 +1,281 @@
package aws
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
dsTypes "github.com/aws/aws-sdk-go-v2/service/directoryservice/types"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type DirectoryModule struct {
// General configuration data
DSClient sdk.AWSDSClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSProfile string
Goroutines int
WrapTable bool
AWSOutputType string
AWSTableCols string
AWSMFAToken string
AWSConfig aws.Config
AWSProfileProvided string
AWSProfileStub string
CloudFoxVersion string
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Directories []Directory
CommandCounter internal.CommandCounter
output internal.OutputData2
modLog *logrus.Entry
}
type Directory struct {
DirectoryId string
DNS string
NetBios string
AccessURL string
Alias string
OsVersion string
Region string
TrustInfo string
}
func (m *DirectoryModule) PrintDirectories(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "directory-services"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfileProvided == "" {
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
} else {
m.AWSProfileStub = m.AWSProfileProvided
}
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileProvided, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s][%s] Enumerating Cloud Directories with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan Directory)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
wg.Wait()
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
// Send a message to the data receiver goroutine to close the channel and stop
receiverDone <- true
<-receiverDone
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Name",
"Alias",
"Domain",
"NetBIOS name",
"Access URL",
"Version",
"Trusts",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Name",
"Alias",
"Domain",
"NetBIOS name",
"Access URL",
"Version",
"Trusts",
}
} else {
tableCols = []string{
"Account",
"Name",
"Domain",
"NetBIOS name",
"Access URL",
"Version",
"Trusts",
}
}
// Table rows
for i := range m.Directories {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Directories[i].DirectoryId,
m.Directories[i].Alias,
m.Directories[i].DNS,
m.Directories[i].NetBios,
m.Directories[i].AccessURL,
m.Directories[i].OsVersion,
m.Directories[i].TrustInfo,
},
)
}
if len(m.output.Body) > 0 {
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfileStub
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s directories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), strconv.Itoa(len(m.output.Body)))
//fmt.Printf("[%s][%s] Resource policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
} else {
fmt.Printf("[%s][%s] No directories found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), m.output.CallingModule)
}
func (m *DirectoryModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
defer wg.Done()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("directoryservice", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getDirectoriesPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *DirectoryModule) Receiver(receiver chan Directory, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Directories = append(m.Directories, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *DirectoryModule) getDirectoriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// Get directories
directories, err := sdk.CachedDSDescribeDirectories(m.DSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err)
}
for _, directory := range directories {
trusts, err := sdk.CachedDSDescribeTrusts(m.DSClient, aws.ToString(m.Caller.Account), r, *directory.DirectoryId)
if err != nil {
m.modLog.Error(err)
}
dataReceiver <- Directory{
DirectoryId: *directory.DirectoryId,
DNS: *directory.Name,
NetBios: *directory.ShortName,
Region: r,
AccessURL: *directory.AccessUrl,
Alias: *directory.Alias,
OsVersion: fmt.Sprintf("%s", directory.OsVersion),
TrustInfo: m.formatTrusts(trusts),
}
}
}
func (m *DirectoryModule) formatTrusts(t []dsTypes.Trust) string {
var output string = ""
for idx, trust := range t {
if idx != 0 {
output = output + "\n"
}
if trust.TrustDirection == "One-Way: Outgoing" {
output = output + "→"
} else if trust.TrustDirection == "One-Way: Ingoing" {
output = output + "←"
} else {
output = output + "↔"
}
output = fmt.Sprintf("%s %s", output, *trust.RemoteDomainName)
// check trust type (external or forest)
if trust.TrustType == "External" {
output = fmt.Sprintf("%s (%s)", output, "Domain")
} else {
output = fmt.Sprintf("%s (%s)", output, "Forest")
}
}
return output
}
+177 -121
View File
@@ -1,7 +1,6 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -10,9 +9,10 @@ import (
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
@@ -21,17 +21,16 @@ import (
type ECRModule struct {
// General configuration data
ECRClient *ecr.Client
// These interfaces are used for unit testing
ECRClientDescribeReposInterface ecr.DescribeRepositoriesAPIClient
ECRClientDescribeImagesInterface ecr.DescribeImagesAPIClient
ECRClient sdk.AWSECRClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Repositories []Repository
@@ -49,10 +48,12 @@ type Repository struct {
PushedAt string
ImageTags string
ImageSize int64
Policy policy.Policy
PolicyJSON string
}
func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ecr"
@@ -83,7 +84,7 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -97,8 +98,9 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
receiverDone <- true
<-receiverDone
// add - if struct is not empty do this. otherwise, dont write anything.
// This is the complete list of potential table columns
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
@@ -108,11 +110,53 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
"ImageSize",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Region",
"Name",
"URI",
"PushedAt",
"ImageTags",
"ImageSize",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"URI",
"PushedAt",
"ImageTags",
"ImageSize",
}
}
// sort the table by Name
sort.Slice(m.Repositories, func(i, j int) bool {
return m.Repositories[i].Name < m.Repositories[j].Name
})
// Table rows
for i := range m.Repositories {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Repositories[i].AWSService,
m.Repositories[i].Region,
m.Repositories[i].Name,
@@ -125,11 +169,25 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath, verbosity)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s repositories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No repositories found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -140,15 +198,19 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ecr", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -172,7 +234,7 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "ecr-pull-commands.txt")
@@ -197,7 +259,7 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -215,8 +277,8 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -224,107 +286,101 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
defer func() {
<-semaphore
}()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var PaginationControl2 *string
for {
DescribeRepositories, err := m.ECRClientDescribeReposInterface.DescribeRepositories(
context.TODO(),
&ecr.DescribeRepositoriesInput{
NextToken: PaginationControl,
},
func(o *ecr.Options) {
o.Region = r
},
)
Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, repo := range Repositories {
repoName := aws.ToString(repo.RepositoryName)
repoURI := aws.ToString(repo.RepositoryUri)
//created := *repo.CreatedAt
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
images, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
m.CommandCounter.IncrError()
continue
}
for _, repo := range DescribeRepositories.Repositories {
repoName := aws.ToString(repo.RepositoryName)
repoURI := aws.ToString(repo.RepositoryUri)
//created := *repo.CreatedAt
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
var images []types.ImageDetail
for {
DescribeImages, err := m.ECRClientDescribeImagesInterface.DescribeImages(
context.TODO(),
&ecr.DescribeImagesInput{
RepositoryName: &repoName,
NextToken: PaginationControl2,
},
func(o *ecr.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
sort.Slice(images, func(i, j int) bool {
return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05")
})
//images := DescribeImages.ImageDetails
images = append(images, DescribeImages.ImageDetails...)
var image types.ImageDetail
var imageTags string
if DescribeImages.NextToken != nil {
PaginationControl2 = DescribeImages.NextToken
} else {
// not sure if this is the right way to do this, but adding this code here was the only way i could
// sort the results from all pages to look for the latest push.
PaginationControl2 = nil
sort.Slice(images, func(i, j int) bool {
return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05")
})
var image types.ImageDetail
var imageTags string
if len(images) > 1 {
image = images[len(images)-1]
} else if len(images) == 1 {
image = images[0]
} else {
break
}
if len(image.ImageTags) > 0 {
imageTags = image.ImageTags[0]
}
//imageTags := image.ImageTags[0]
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
imageSize := aws.ToInt64(image.ImageSizeInBytes)
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
dataReceiver <- Repository{
AWSService: "ECR",
Name: repoName,
Region: r,
URI: pullURI,
PushedAt: pushedAt,
ImageTags: imageTags,
ImageSize: imageSize,
}
// }
break
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if DescribeRepositories.NextToken != nil {
PaginationControl = DescribeRepositories.NextToken
if len(images) > 1 {
image = images[len(images)-1]
} else if len(images) == 1 {
image = images[0]
} else {
PaginationControl = nil
break
continue
}
if len(image.ImageTags) > 0 {
imageTags = image.ImageTags[0]
} else {
imageTags = "No tags"
}
//imageTags := image.ImageTags[0]
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
imageSize := aws.ToInt64(image.ImageSizeInBytes)
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
dataReceiver <- Repository{
AWSService: "ECR",
Name: repoName,
Region: r,
URI: pullURI,
PushedAt: pushedAt,
ImageTags: imageTags,
ImageSize: imageSize,
}
}
}
// func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) {
// var repositories []types.Repository
// Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
// if err != nil {
// m.CommandCounter.IncrError()
// return nil, err
// }
// repositories = append(repositories, Repositories...)
// return repositories, nil
// }
// func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) {
// var images []types.ImageDetail
// ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
// if err != nil {
// m.CommandCounter.IncrError()
// return nil, err
// }
// images = append(images, ImageDetails...)
// return images, nil
// }
func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.Policy, error) {
var repoPolicy policy.Policy
Policy, err := sdk.CachedECRGetRepositoryPolicy(m.ECRClient, aws.ToString(m.Caller.Account), r, repository)
if err != nil {
m.CommandCounter.IncrError()
return repoPolicy, err
}
repoPolicy, err = policy.ParseJSONPolicy([]byte(Policy))
if err != nil {
return repoPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", repository, err)
}
return repoPolicy, nil
}
+27 -52
View File
@@ -1,52 +1,17 @@
package aws
import (
"context"
"log"
"testing"
"time"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"}
type MockedECRClientDescribeRepos struct {
AWSRegions []string
}
type MockedECRClientDescribeImages struct {
AWSRegions []string
}
func (m *MockedECRClientDescribeRepos) DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
return &ecr.DescribeRepositoriesOutput{
Repositories: []types.Repository{
{
RepositoryName: aws.String("test1"),
RepositoryUri: aws.String("testURI"),
},
},
}, nil
}
func (m *MockedECRClientDescribeImages) DescribeImages(context.Context, *ecr.DescribeImagesInput, ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
return &ecr.DescribeImagesOutput{
ImageDetails: []types.ImageDetail{
{
ImagePushedAt: aws.Time(time.Now()),
ImageSizeInBytes: aws.Int64(123456),
ImageTags: []string{"latest"},
},
},
}, nil
}
func TestDescribeRepos(t *testing.T) {
subtests := []struct {
name string
outputDirectory string
@@ -59,27 +24,37 @@ func TestDescribeRepos(t *testing.T) {
outputDirectory: ".",
verbosity: 2,
testModule: ECRModule{
ECRClientDescribeReposInterface: &MockedECRClientDescribeRepos{},
ECRClientDescribeImagesInterface: &MockedECRClientDescribeImages{},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
ECRClient: &sdk.MockedECRClient{},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
AWSProfile: "test",
Goroutines: 30,
AWSRegions: []string{"us-east-1"},
},
expectedResult: []Repository{{
Name: "test1",
URI: "testURI:latest",
PushedAt: "2022-10-25 15:14:06",
ImageTags: "latest",
ImageSize: 123456,
}},
expectedResult: []Repository{
{
Name: "repo1",
URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1",
PushedAt: "2022-10-25 15:14:00",
ImageTags: "customtag, tag2",
ImageSize: 123456,
},
{
Name: "repo2",
URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2",
PushedAt: "2021-10-15 11:14:00",
ImageTags: "latest",
ImageSize: 2222222,
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintECR(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
subtest.testModule.PrintECR(subtest.outputDirectory, subtest.verbosity)
for index, expectedRepo := range subtest.expectedResult {
if expectedRepo.Name != subtest.testModule.Repositories[index].Name {
log.Fatal("Repo name does not match expected name")
+220 -153
View File
@@ -2,6 +2,7 @@ package aws
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -9,35 +10,32 @@ import (
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ecs"
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type DescribeTasksDefinitionAPIClient interface {
DescribeTaskDefinition(context.Context, *ecs.DescribeTaskDefinitionInput, ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
}
type ECSTasksModule struct {
DescribeTaskDefinitionClient DescribeTasksDefinitionAPIClient
DescribeTasksClient ecs.DescribeTasksAPIClient
ListTasksClient ecs.ListTasksAPIClient
ListClustersClient ecs.ListClustersAPIClient
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
ECSClient sdk.AWSECSClientInterface
EC2Client sdk.AWSEC2ClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
AWSProfile string
Goroutines int
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
@@ -50,18 +48,20 @@ type ECSTasksModule struct {
}
type MappedECSTask struct {
Cluster string
TaskDefinition string
LaunchType string
ID string
ExternalIP string
PrivateIP string
Role string
Admin string
CanPrivEsc string
Cluster string
TaskDefinitionName string
TaskDefinitionContent string
ContainerName string
LaunchType string
ID string
ExternalIP string
PrivateIP string
Role string
Admin string
CanPrivEsc string
}
func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, verbosity int) {
func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ecs-tasks"
@@ -75,15 +75,15 @@ func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, v
fmt.Printf("[%s][%s] Enumerating ECS tasks in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
if m.pmapperError != nil {
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
} else {
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// } else {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
wg := new(sync.WaitGroup)
@@ -100,7 +100,7 @@ func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, v
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, dataReceiver)
}
@@ -124,7 +124,7 @@ func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, v
receiverDone <- true
<-receiverDone
m.printECSTaskData(outputFormat, outputDirectory, dataReceiver)
m.printECSTaskData(outputDirectory, dataReceiver, verbosity)
}
@@ -141,10 +141,13 @@ func (m *ECSTasksModule) Receiver(receiver chan MappedECSTask, receiverDone chan
}
}
func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory string, dataReceiver chan MappedECSTask) {
func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) {
// This is the complete list of potential table columns
m.output.Headers = []string{
"Account",
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"ID",
"External IP",
@@ -154,12 +157,61 @@ func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory s
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"ID",
"External IP",
"Internal IP",
"RoleArn",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"External IP",
"Internal IP",
"RoleArn",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
for _, ecsTask := range m.MappedECSTasks {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
ecsTask.Cluster,
ecsTask.TaskDefinition,
ecsTask.TaskDefinitionName,
ecsTask.ContainerName,
ecsTask.LaunchType,
ecsTask.ID,
ecsTask.ExternalIP,
@@ -170,12 +222,26 @@ func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory s
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath)
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName)
fmt.Printf("[%s][%s] %s ECS tasks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -188,7 +254,7 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
privateIPsFilename := filepath.Join(path, "ecs-tasks-PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "ecs-tasks-PublicIPs.txt")
@@ -208,12 +274,30 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, task := range m.MappedECSTasks {
if task.TaskDefinitionContent != "" {
path := filepath.Join(path, "task-definitions")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
taskDefinitionFilename := filepath.Join(path, task.TaskDefinitionName+".json")
err = os.WriteFile(taskDefinitionFilename, []byte(task.TaskDefinitionContent), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
}
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
@@ -224,8 +308,12 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedECSTask) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ecs", r)
if err != nil {
@@ -233,86 +321,48 @@ func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceive
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getListClusters(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
}
func (m *ECSTasksModule) getListClusters(region string, dataReceiver chan MappedECSTask) {
var PaginationControl *string
for {
ListClusters, err := m.ListClustersClient.ListClusters(
context.TODO(),
&(ecs.ListClustersInput{
NextToken: PaginationControl,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, clusterARN := range ListClusters.ClusterArns {
m.getListTasks(clusterARN, region, dataReceiver)
}
if ListClusters.NextToken != nil {
PaginationControl = ListClusters.NextToken
} else {
PaginationControl = nil
break
}
ClusterArns, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, clusterARN := range ClusterArns {
m.getListTasks(clusterARN, region, dataReceiver)
}
}
func (m *ECSTasksModule) getListTasks(clusterARN string, region string, dataReceiver chan MappedECSTask) {
var PaginationControl *string
for {
ListTasks, err := m.ListTasksClient.ListTasks(
context.TODO(),
&(ecs.ListTasksInput{
Cluster: aws.String(clusterARN),
NextToken: PaginationControl,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax
for i := 0; i < len(ListTasks.TaskArns); i += batchSize {
j := i + batchSize
if j > len(ListTasks.TaskArns) {
j = len(ListTasks.TaskArns)
}
m.loadTasksData(clusterARN, ListTasks.TaskArns[i:j], region, dataReceiver)
}
if ListTasks.NextToken != nil {
PaginationControl = ListTasks.NextToken
} else {
PaginationControl = nil
break
}
TaskArns, err := sdk.CachedECSListTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax
for i := 0; i < len(TaskArns); i += batchSize {
j := i + batchSize
if j > len(TaskArns) {
j = len(TaskArns)
}
m.loadTasksData(clusterARN, TaskArns[i:j], region, dataReceiver)
}
}
func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, region string, dataReceiver chan MappedECSTask) {
@@ -321,24 +371,15 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
return
}
DescribeTasks, err := m.DescribeTasksClient.DescribeTasks(
context.TODO(),
&(ecs.DescribeTasksInput{
Cluster: aws.String(clusterARN),
Tasks: taskARNs,
}),
func(o *ecs.Options) {
o.Region = region
},
)
Tasks, err := sdk.CachedECSDescribeTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN, taskARNs)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
eniIDs := []string{}
for _, task := range DescribeTasks.Tasks {
var eniIDs []string
for _, task := range Tasks {
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
if eniID != "" {
eniIDs = append(eniIDs, eniID)
@@ -347,18 +388,27 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
publicIPs, err := m.loadPublicIPs(eniIDs, region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
for _, task := range DescribeTasks.Tasks {
for _, task := range Tasks {
//taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region)
taskDefinition, err := sdk.CachedECSDescribeTaskDefinition(m.ECSClient, aws.ToString(m.Caller.Account), region, aws.ToString(task.TaskDefinitionArn))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
mappedTask := MappedECSTask{
Cluster: getNameFromARN(clusterARN),
TaskDefinition: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
LaunchType: string(task.LaunchType),
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
Role: m.getTaskRole(aws.ToString(task.TaskDefinitionArn), region),
Cluster: getNameFromARN(clusterARN),
TaskDefinitionName: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
TaskDefinitionContent: getTaskDefinitionContent(taskDefinition),
ContainerName: getContainerNamesFromECSTask(task),
LaunchType: string(task.LaunchType),
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
Role: getTaskRole(taskDefinition),
}
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
@@ -370,8 +420,22 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
}
}
func (m *ECSTasksModule) getTaskRole(taskDefinitionArn string, region string) string {
DescribeTaskDefinition, err := m.DescribeTaskDefinitionClient.DescribeTaskDefinition(
func getTaskRole(taskDefinition types.TaskDefinition) string {
return aws.ToString(taskDefinition.TaskRoleArn)
}
func getTaskDefinitionContent(taskDefinition types.TaskDefinition) string {
// return taskDefinition as a json string
taskDefinitionContent, err := json.Marshal(taskDefinition)
if err != nil {
return ""
}
return string(taskDefinitionContent)
}
func (m *ECSTasksModule) describeTaskDefinition(taskDefinitionArn string, region string) (types.TaskDefinition, error) {
DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition(
context.TODO(),
&ecs.DescribeTaskDefinitionInput{
TaskDefinition: &taskDefinitionArn,
@@ -382,10 +446,10 @@ func (m *ECSTasksModule) getTaskRole(taskDefinitionArn string, region string) st
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return ""
m.CommandCounter.IncrError()
return types.TaskDefinition{}, err
}
return aws.ToString(DescribeTaskDefinition.TaskDefinition.TaskRoleArn)
return *DescribeTaskDefinition.TaskDefinition, nil
}
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF NOT NEEDED
@@ -402,7 +466,7 @@ func (m *ECSTasksModule) loadAllPublicIPs(eniIDs []string, region string) (map[s
publicIPs, err := m.loadPublicIPs(eniIDs[i:j], region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
}
@@ -421,20 +485,13 @@ func (m *ECSTasksModule) loadPublicIPs(eniIDs []string, region string) (map[stri
if len(eniIDs) == 0 {
return eniPublicIPs, nil
}
DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
context.TODO(),
&(ec2.DescribeNetworkInterfacesInput{
NetworkInterfaceIds: eniIDs,
}),
func(o *ec2.Options) {
o.Region = region
},
)
NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region)
if err != nil {
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
}
for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces {
for _, eni := range NetworkInterfaces {
eniPublicIPs[aws.ToString(eni.NetworkInterfaceId)] = getPublicIPOfElasticNetworkInterface(eni)
}
@@ -459,8 +516,18 @@ func getIDFromECSTask(arn string) string {
return tokens[2]
}
func getContainerNamesFromECSTask(task types.Task) string {
var names []string
for _, container := range task.Containers {
names = append(names, aws.ToString(container.Name))
}
return strings.Join(names, "|")
}
func getPrivateIPv4AddressFromECSTask(task types.Task) string {
ips := []string{}
var ips []string
for _, attachment := range task.Attachments {
if aws.ToString(attachment.Type) != "ElasticNetworkInterface" || aws.ToString(attachment.Status) != "ATTACHED" {
+17 -252
View File
@@ -1,249 +1,15 @@
package aws
import (
"context"
"encoding/json"
"log"
"os"
"testing"
"time"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json"
const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json"
type ListTasks struct {
TaskArns []string `json:"taskArns"`
}
type DescribeTasks struct {
Tasks []struct {
Attachments []struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
Details []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"details"`
} `json:"attachments"`
Attributes []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"attributes"`
AvailabilityZone string `json:"availabilityZone"`
ClusterArn string `json:"clusterArn"`
Connectivity string `json:"connectivity"`
ConnectivityAt string `json:"connectivityAt"`
Containers []struct {
ContainerArn string `json:"containerArn"`
TaskArn string `json:"taskArn"`
Name string `json:"name"`
Image string `json:"image"`
RuntimeID string `json:"runtimeId"`
LastStatus string `json:"lastStatus"`
NetworkBindings []interface{} `json:"networkBindings"`
NetworkInterfaces []struct {
AttachmentID string `json:"attachmentId"`
PrivateIpv4Address string `json:"privateIpv4Address"`
} `json:"networkInterfaces"`
HealthStatus string `json:"healthStatus"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
} `json:"containers"`
CPU string `json:"cpu"`
CreatedAt string `json:"createdAt"`
DesiredStatus string `json:"desiredStatus"`
EnableExecuteCommand bool `json:"enableExecuteCommand"`
Group string `json:"group"`
HealthStatus string `json:"healthStatus"`
LastStatus string `json:"lastStatus"`
LaunchType string `json:"launchType"`
Memory string `json:"memory"`
Overrides struct {
ContainerOverrides []struct {
Name string `json:"name"`
} `json:"containerOverrides"`
InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"`
} `json:"overrides"`
PlatformVersion string `json:"platformVersion"`
PlatformFamily string `json:"platformFamily"`
PullStartedAt string `json:"pullStartedAt"`
PullStoppedAt string `json:"pullStoppedAt"`
StartedAt string `json:"startedAt"`
StartedBy string `json:"startedBy"`
Tags []interface{} `json:"tags"`
TaskArn string `json:"taskArn"`
TaskDefinitionArn string `json:"taskDefinitionArn"`
Version int `json:"version"`
EphemeralStorage struct {
SizeInGiB int `json:"sizeInGiB"`
} `json:"ephemeralStorage"`
} `json:"tasks"`
Failures []interface{} `json:"failures"`
}
type DescribeNetworkInterfaces struct {
NetworkInterfaces []struct {
Status string `json:"Status"`
MacAddress string `json:"MacAddress"`
SourceDestCheck bool `json:"SourceDestCheck"`
VpcID string `json:"VpcId"`
Description string `json:"Description"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
NetworkInterfaceID string `json:"NetworkInterfaceId"`
PrivateIPAddresses []struct {
PrivateDNSName string `json:"PrivateDnsName"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
Primary bool `json:"Primary"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"PrivateIpAddresses"`
RequesterManaged bool `json:"RequesterManaged"`
Ipv6Addresses []interface{} `json:"Ipv6Addresses"`
PrivateDNSName string `json:"PrivateDnsName,omitempty"`
AvailabilityZone string `json:"AvailabilityZone"`
Attachment struct {
Status string `json:"Status"`
DeviceIndex int `json:"DeviceIndex"`
AttachTime time.Time `json:"AttachTime"`
InstanceID string `json:"InstanceId"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
AttachmentID string `json:"AttachmentId"`
InstanceOwnerID string `json:"InstanceOwnerId"`
} `json:"Attachment"`
Groups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"Groups"`
SubnetID string `json:"SubnetId"`
OwnerID string `json:"OwnerId"`
TagSet []interface{} `json:"TagSet"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"NetworkInterfaces"`
}
func readTestFile(testFile string) []byte {
file, err := os.ReadFile(testFile)
if err != nil {
log.Fatalf("can't read file %s", testFile)
}
return file
}
type mockedListclustersClient struct {
}
func (c *mockedListclustersClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{ClusterArns: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3",
}}, nil
}
type mockedListTasksClient struct{}
func (c *mockedListTasksClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
return &ecs.ListTasksOutput{TaskArns: []string{
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
}}, nil
}
type mockedDescribeTasksClient struct {
describeTasks DescribeTasks
}
func (c *mockedDescribeTasksClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE)
}
var tasks []ecsTypes.Task
for _, mockedTask := range c.describeTasks.Tasks {
if mockedTask.ClusterArn == aws.ToString(input.Cluster) {
for _, inputTask := range input.Tasks {
if mockedTask.TaskArn == inputTask {
var attachments []ecsTypes.Attachment
for _, a := range mockedTask.Attachments {
var deets []ecsTypes.KeyValuePair
for _, detail := range a.Details {
deets = append(deets, ecsTypes.KeyValuePair{
Name: aws.String(detail.Name),
Value: aws.String(detail.Value)})
}
attachments = append(attachments, ecsTypes.Attachment{
Type: aws.String(a.Type),
Details: deets,
Id: aws.String(a.ID),
Status: aws.String(a.Status),
})
}
tasks = append(tasks, ecsTypes.Task{
ClusterArn: aws.String(mockedTask.ClusterArn),
TaskDefinitionArn: aws.String(mockedTask.TaskDefinitionArn),
LaunchType: ecsTypes.LaunchType(*aws.String(mockedTask.LaunchType)),
TaskArn: aws.String(mockedTask.TaskArn),
Attachments: attachments,
})
}
}
}
}
return &ecs.DescribeTasksOutput{Tasks: tasks}, nil
}
type mockedDescribeNetworkInterfacesClient struct {
describeNetworkInterfaces DescribeNetworkInterfaces
}
func (c *mockedDescribeNetworkInterfacesClient) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
var nics []ec2types.NetworkInterface
err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
}
for _, mockedNic := range c.describeNetworkInterfaces.NetworkInterfaces {
for _, inputNicID := range input.NetworkInterfaceIds {
if mockedNic.NetworkInterfaceID == inputNicID {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockedNic.Association.PublicIP),
},
NetworkInterfaceId: aws.String(mockedNic.NetworkInterfaceID)})
}
}
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
type mockedDescribeTaskDefinitionInterface struct {
}
func (c *mockedDescribeTaskDefinitionInterface) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) {
testTaskDefinition := ecsTypes.TaskDefinition{}
testTaskDefinition.TaskRoleArn = aws.String("test123")
return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil
}
func TestECSTasks(t *testing.T) {
subtests := []struct {
name string
@@ -257,35 +23,34 @@ func TestECSTasks(t *testing.T) {
outputDirectory: ".",
verbosity: 2,
testModule: ECSTasksModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
SkipAdminCheck: true,
Goroutines: 30,
DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient{},
DescribeTasksClient: &mockedDescribeTasksClient{},
ListTasksClient: &mockedListTasksClient{},
ListClustersClient: &mockedListclustersClient{},
DescribeTaskDefinitionClient: &mockedDescribeTaskDefinitionInterface{},
//IAMSimulatePrincipalPolicyClient: &mockedDescribeTaskDefinitionsClient{},
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
SkipAdminCheck: true,
Goroutines: 30,
EC2Client: &sdk.MockedEC2Client2{},
ECSClient: &sdk.MockedECSClient{},
},
expectedResult: []MappedECSTask{{
Cluster: "MyCluster",
ID: "74de0355a10a4f979ac495c14EXAMPLE",
ExternalIP: "203.0.113.12",
Role: "test123",
Cluster: "MyCluster",
ID: "74de0355a10a4f979ac495c14EXAMPLE",
ContainerName: "web",
ExternalIP: "203.0.113.12",
Role: "test123",
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.ECSTasks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
subtest.testModule.ECSTasks(subtest.outputDirectory, subtest.verbosity)
for index, expectedTask := range subtest.expectedResult {
if expectedTask.Cluster != subtest.testModule.MappedECSTasks[index].Cluster {
log.Fatal("Cluster name does not match expected value")
}
if expectedTask.ContainerName != subtest.testModule.MappedECSTasks[index].ContainerName {
log.Fatal("Container name does not match expected value")
}
}
})
}
+119 -155
View File
@@ -1,18 +1,16 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/eks"
"github.com/aws/aws-sdk-go-v2/service/eks/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
@@ -21,19 +19,20 @@ import (
type EKSModule struct {
// General configuration data
// These interfaces are used for unit testing
EKSClientListClustersInterface eks.ListClustersAPIClient
EKSClientDescribeClusterInterface eks.DescribeClusterAPIClient
EKSClientListNodeGroupsInterface eks.ListNodegroupsAPIClient
EKSClientDescribeNodeGroupInterface eks.DescribeNodegroupAPIClient
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
EKSClient sdk.EKSClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
@@ -58,8 +57,8 @@ type Cluster struct {
CanPrivEsc string
}
func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "eks"
@@ -74,15 +73,15 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
fmt.Printf("[%s][%s] Enumerating EKS clusters for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
if m.pmapperError != nil {
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
} else {
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// } else {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
wg := new(sync.WaitGroup)
@@ -103,7 +102,7 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -128,9 +127,9 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
}
}
// add - if struct is not empty do this. otherwise, dont write anything.
// This is the complete list of potential table columns
m.output.Headers = []string{
"Service",
"Account",
"Region",
"Name",
//"Endpoint",
@@ -142,12 +141,59 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Name",
"Endpoint",
"Public",
"OIDC",
"NodeGroup",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Region",
"Name",
//"Endpoint",
"Public",
//"OIDC",
"NodeGroup",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
// Table rows
for i := range m.Clusters {
m.output.Body = append(
m.output.Body,
[]string{
m.Clusters[i].AWSService,
aws.ToString(m.Caller.Account),
m.Clusters[i].Region,
m.Clusters[i].Name,
//m.Clusters[i].Endpoint,
@@ -159,7 +205,6 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
m.Clusters[i].CanPrivEsc,
},
)
}
var seen []string
@@ -170,11 +215,24 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath, verbosity)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %d clusters with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body))
} else {
fmt.Printf("[%s][%s] No clusters found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -185,15 +243,19 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
func (m *EKSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("eks", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getEKSRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -217,7 +279,7 @@ func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "eks-kubeconfig-commands.txt")
@@ -241,7 +303,7 @@ func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -260,8 +322,8 @@ func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -270,40 +332,48 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
<-semaphore
}()
var clusters []string
var role string
clusters, err := m.listClusters(r)
clusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
for _, clusterName := range clusters {
clusterDetails, err := m.describeCluster(clusterName, r)
var role string
var oidc string = ""
var publicEndpoint = ""
clusterDetails, err := sdk.CachedEKSDescribeCluster(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
//nodeGroups = append(nodeGroups, DescribeCluster.Cluster.)
endpoint := aws.ToString(clusterDetails.Endpoint)
oidc := aws.ToString(clusterDetails.Identity.Oidc.Issuer)
publicEndpoint := strconv.FormatBool(clusterDetails.ResourcesVpcConfig.EndpointPublicAccess)
if clusterDetails.Identity != nil && clusterDetails.Identity.Oidc != nil {
oidc = aws.ToString(clusterDetails.Identity.Oidc.Issuer)
}
if clusterDetails.ResourcesVpcConfig != nil {
publicEndpoint = strconv.FormatBool(clusterDetails.ResourcesVpcConfig.EndpointPublicAccess)
}
// if DescribeCluster.Cluster.ResourcesVpcConfig.PublicAccessCidrs[0] == "0.0.0.0/0" {
// publicCIDRs := "0.0.0.0/0"
// } else {
// publicCIDRs := "specific IPs"
// }
ListNodeGroups := m.listNodeGroups(clusterName, r)
ListNodeGroups, err := sdk.CachedEKSListNodeGroups(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
if len(ListNodeGroups) > 0 {
for _, nodeGroup := range ListNodeGroups {
nodeGroupDetails, err := m.describeNodegroup(clusterName, nodeGroup, r)
nodeGroupDetails, err := sdk.CachedEKSDescribeNodeGroup(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, nodeGroup, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
role = aws.ToString(nodeGroupDetails.NodeRole)
@@ -341,109 +411,3 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
}
}
func (m *EKSModule) listClusters(r string) ([]string, error) {
var PaginationControl *string
var clusters []string
for {
ListClusters, err := m.EKSClientListClustersInterface.ListClusters(
context.TODO(),
&eks.ListClustersInput{
NextToken: PaginationControl,
},
func(o *eks.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return clusters, err
}
clusters = append(clusters, ListClusters.Clusters...)
// The "NextToken" value is nil when there's no more data to return.
if ListClusters.NextToken != nil {
PaginationControl = ListClusters.NextToken
} else {
PaginationControl = nil
break
}
}
return clusters, nil
}
func (m *EKSModule) listNodeGroups(clusterName string, r string) []string {
var PaginationControl *string
var nodeGroups []string
for {
ListNodeGroups, err := m.EKSClientListNodeGroupsInterface.ListNodegroups(
context.TODO(),
&eks.ListNodegroupsInput{
ClusterName: &clusterName,
NextToken: PaginationControl,
},
func(o *eks.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
nodeGroups = append(nodeGroups, ListNodeGroups.Nodegroups...)
if ListNodeGroups.NextToken != nil {
PaginationControl = ListNodeGroups.NextToken
} else {
PaginationControl = nil
break
}
}
return nodeGroups
}
func (m *EKSModule) describeCluster(clusterName string, r string) (*types.Cluster, error) {
var err error
//var clusterDetails types.Cluster
DescribeCluster, err := m.EKSClientDescribeClusterInterface.DescribeCluster(
context.TODO(),
&eks.DescribeClusterInput{
Name: &clusterName,
},
func(o *eks.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
return DescribeCluster.Cluster, err
}
func (m *EKSModule) describeNodegroup(clusterName string, nodeGroup string, r string) (*types.Nodegroup, error) {
DescribeNodegroup, err := m.EKSClientDescribeNodeGroupInterface.DescribeNodegroup(
context.TODO(),
&eks.DescribeNodegroupInput{
ClusterName: &clusterName,
NodegroupName: &nodeGroup,
},
func(o *eks.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
return DescribeNodegroup.Nodegroup, err
}
+7 -23
View File
@@ -12,22 +12,10 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sts"
)
type MockedEKSClientListClusters struct {
type MockedEKSClientInterface struct {
}
type MockedEKSClientDescribeCluster struct {
}
type MockedEKSClientListNodegroups struct {
}
type MockedEKSClientDescribeNodegroup struct {
}
type MockedIAMSimulatePrincipalPolicy struct {
}
func (m *MockedEKSClientListClusters) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
func (m *MockedEKSClientInterface) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
testClusters := []string{}
testClusters = append(testClusters, "test1")
@@ -38,7 +26,7 @@ func (m *MockedEKSClientListClusters) ListClusters(ctx context.Context, params *
return testListClustersOutput, nil
}
func (m *MockedEKSClientDescribeCluster) DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
func (m *MockedEKSClientInterface) DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
testIdentity := types.Identity{
Oidc: &types.OIDC{
Issuer: aws.String("abc123"),
@@ -61,7 +49,7 @@ func (m *MockedEKSClientDescribeCluster) DescribeCluster(context.Context, *eks.D
return testDescribeClusterOutput, nil
}
func (m *MockedEKSClientListNodegroups) ListNodegroups(ctx context.Context, params *eks.ListNodegroupsInput, optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
func (m *MockedEKSClientInterface) ListNodegroups(ctx context.Context, params *eks.ListNodegroupsInput, optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
testNodegroups := []string{}
testNodegroups = append(testNodegroups, "test1")
@@ -72,7 +60,7 @@ func (m *MockedEKSClientListNodegroups) ListNodegroups(ctx context.Context, para
return testListNodegroupsOutput, nil
}
func (m *MockedEKSClientDescribeNodegroup) DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
func (m *MockedEKSClientInterface) DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
testNodegroup := types.Nodegroup{}
testNodegroup.NodeRole = aws.String("roleABC")
testDescribeNodegroupOutput := &eks.DescribeNodegroupOutput{
@@ -95,13 +83,9 @@ func TestEks(t *testing.T) {
outputDirectory: ".",
verbosity: 2,
testModule: EKSModule{
EKSClientListClustersInterface: &MockedEKSClientListClusters{},
EKSClientDescribeClusterInterface: &MockedEKSClientDescribeCluster{},
EKSClientListNodeGroupsInterface: &MockedEKSClientListNodegroups{},
EKSClientDescribeNodeGroupInterface: &MockedEKSClientDescribeNodegroup{},
EKSClient: &MockedEKSClientInterface{},
//IAMSimulatePrincipalPolicyClient: iam.SimulatePrincipalPolicyAPIClient,
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
@@ -119,7 +103,7 @@ func TestEks(t *testing.T) {
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.EKS(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
subtest.testModule.EKS(subtest.outputDirectory, subtest.verbosity)
for index, expectedCluster := range subtest.expectedResult {
if expectedCluster.Name != subtest.testModule.Clusters[index].Name {
log.Fatal("Cluster name does not match expected value")
+122 -68
View File
@@ -1,16 +1,16 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
@@ -18,14 +18,16 @@ import (
)
type ElasticNetworkInterfacesModule struct {
//EC2Client *ec2.Client
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
EC2Client sdk.AWSEC2ClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
MappedENIs []MappedENI
CommandCounter internal.CommandCounter
@@ -44,7 +46,7 @@ type MappedENI struct {
Description string
}
func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat string, outputDirectory string, verbosity int) {
func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "elastic-network-interfaces"
@@ -71,7 +73,7 @@ func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat s
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, dataReceiver)
}
@@ -84,7 +86,7 @@ func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat s
receiverDone <- true
<-receiverDone
m.printENIsData(outputFormat, outputDirectory, dataReceiver)
m.printENIsData(outputDirectory, dataReceiver, verbosity)
}
@@ -101,8 +103,9 @@ func (m *ElasticNetworkInterfacesModule) Receiver(receiver chan MappedENI, recei
}
}
func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outputDirectory string, dataReceiver chan MappedENI) {
func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, dataReceiver chan MappedENI, verbosity int) {
m.output.Headers = []string{
"Account",
"ID",
"Type",
"External IP",
@@ -111,10 +114,48 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outp
"Attached Instance",
"Description",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"ID",
"Type",
"External IP",
"Internal IP",
"VPC ID",
"Attached Instance",
"Description",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"ID",
"Type",
"External IP",
"Internal IP",
"VPC ID",
"Attached Instance",
"Description",
}
}
for _, eni := range m.MappedENIs {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
eni.ID,
eni.Type,
eni.ExternalIP,
@@ -126,11 +167,25 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outp
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName)
fmt.Printf("[%s][%s] %s elastic network interfaces found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -144,7 +199,7 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
privateIPsFilename := filepath.Join(path, "elastic-network-interfaces-PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "elastic-network-interfaces-PublicIPs.txt")
@@ -164,12 +219,12 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
@@ -179,68 +234,67 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getDescribeNetworkInterfaces(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
}
func (m *ElasticNetworkInterfacesModule) getDescribeNetworkInterfaces(region string, dataReceiver chan MappedENI) {
var PaginationControl *string
for {
DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
context.TODO(),
&(ec2.DescribeNetworkInterfacesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces {
status := string(eni.Status)
if status == "available" {
continue // unused ENI
}
mappedENI := MappedENI{
ID: aws.ToString(eni.NetworkInterfaceId),
Type: string(eni.InterfaceType),
ExternalIP: getPublicIPOfElasticNetworkInterface(eni),
PrivateIP: aws.ToString(eni.PrivateIpAddress),
VPCID: aws.ToString(eni.VpcId),
AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni),
Description: aws.ToString(eni.Description),
}
dataReceiver <- mappedENI
}
if DescribeNetworkInterfaces.NextToken != nil {
PaginationControl = DescribeNetworkInterfaces.NextToken
} else {
PaginationControl = nil
break
}
NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region)
// var PaginationControl *string
// for {
// DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
// context.TODO(),
// &(ec2.DescribeNetworkInterfacesInput{
// NextToken: PaginationControl,
// }),
// func(o *ec2.Options) {
// o.Region = region
// },
// )
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, eni := range NetworkInterfaces {
status := string(eni.Status)
if status == "available" {
continue // unused ENI
}
mappedENI := MappedENI{
ID: aws.ToString(eni.NetworkInterfaceId),
Type: string(eni.InterfaceType),
ExternalIP: getPublicIPOfElasticNetworkInterface(eni),
PrivateIP: aws.ToString(eni.PrivateIpAddress),
VPCID: aws.ToString(eni.VpcId),
AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni),
Description: aws.ToString(eni.Description),
}
dataReceiver <- mappedENI
}
}
func getPublicIPOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string {
+44 -33
View File
@@ -1,48 +1,59 @@
package aws
import (
"context"
"encoding/json"
"log"
"testing"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
type mockedDescribeNetworkInterfacesClient2 struct {
describeNetworkInterfaces DescribeNetworkInterfaces
}
func TestElasticNetworkInterfaces(t *testing.T) {
func (c *mockedDescribeNetworkInterfacesClient2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
var nics []ec2types.NetworkInterface
err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
m := ElasticNetworkInterfacesModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
EC2Client: &sdk.MockedEC2Client2{},
}
for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockednic.Association.PublicIP),
//m.ElasticNetworkInterfaces("table", ".", 3)
subtests := []struct {
name string
testModule ElasticNetworkInterfacesModule
expectedResult []MappedENI
}{
{
name: "Test ElasticNetworkInterfaces",
testModule: m,
expectedResult: []MappedENI{
{
PrivateIP: "10.0.1.17",
ExternalIP: "203.0.113.12",
},
{
PrivateIP: "10.0.1.149",
ExternalIP: "198.51.100.0",
},
},
NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID),
PrivateIpAddress: aws.String(mockednic.PrivateIPAddress),
VpcId: aws.String(mockednic.VpcID),
Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)},
Description: aws.String(mockednic.Description),
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.ElasticNetworkInterfaces(".", 3)
for index, expectedTask := range subtest.expectedResult {
if expectedTask.ExternalIP != subtest.testModule.MappedENIs[index].ExternalIP {
t.Errorf("expected %s, got %s", expectedTask.ExternalIP, subtest.testModule.MappedENIs[index].ExternalIP)
}
if expectedTask.PrivateIP != subtest.testModule.MappedENIs[index].PrivateIP {
t.Errorf("expected %s, got %s", expectedTask.PrivateIP, subtest.testModule.MappedENIs[index].PrivateIP)
}
}
})
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
func TestElasticNetworkInterfaces(t *testing.T) {
m := ElasticNetworkInterfacesModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient2{},
}
m.ElasticNetworkInterfaces("table", ".", 3)
}
+977 -960
View File
File diff suppressed because it is too large. Load diff
+281 -201
View File
@@ -6,8 +6,10 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apprunner"
@@ -33,12 +35,15 @@ import (
type EnvsModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSProfile string
OutputFormat string
Goroutines int
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSProfile string
AWSOutputType string
AWSTableCols string
Goroutines int
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Service Clients
ECSClient *ecs.Client
@@ -58,14 +63,16 @@ type EnvsModule struct {
type EnvironmentVariable struct {
service string
taskDefinition string
name string
region string
environmentVarName string
environmentVarValue string
interesting bool
}
func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "env-vars"
@@ -98,7 +105,7 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -118,6 +125,7 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
// Table headers
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
@@ -125,24 +133,106 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
"Value",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Region",
"Name",
"Key",
"Value",
}
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"Key",
"Value",
}
}
m.determineIfInteresting()
//Table rows
for _, envVar := range m.EnvironmentVariables {
m.output.Body = append(
m.output.Body, []string{
envVar.service,
envVar.region,
envVar.name,
envVar.environmentVarName,
envVar.environmentVarValue,
},
)
var finalName string
if envVar.taskDefinition != "" {
finalName = fmt.Sprintf("%s/%s", envVar.name, envVar.taskDefinition)
} else {
finalName = envVar.name
}
if envVar.interesting {
m.output.Body = append(
m.output.Body, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
finalName,
magenta(envVar.environmentVarName),
magenta(envVar.environmentVarValue),
},
)
} else {
m.output.Body = append(
m.output.Body, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
finalName,
envVar.environmentVarName,
envVar.environmentVarValue,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
// Create another table file that only contains the environment variables that seem to be secrets.
body := m.interestingEnvVarsOnly()
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: body,
TableCols: tableCols,
Name: fmt.Sprintf("%s-interesting", m.output.CallingModule),
SkipPrintToScreen: true,
})
o.WriteFullOutput(o.Table.TableFiles, nil)
fmt.Printf("[%s][%s] %s environment variables found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -151,6 +241,20 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *EnvsModule) determineIfInteresting() {
for i, envVar := range m.EnvironmentVariables {
// If the environment variable name contains the word secret, key, token, or password, we'll make it magenta to make it stand out.
lowerEnvVar := strings.ToLower(envVar.environmentVarName)
if (strings.Contains(lowerEnvVar, "secret") && !strings.Contains(lowerEnvVar, "arn")) ||
strings.Contains(lowerEnvVar, "key") ||
strings.Contains(lowerEnvVar, "token") ||
strings.Contains(lowerEnvVar, "user") ||
strings.Contains(lowerEnvVar, "pass") {
m.EnvironmentVariables[i].interesting = true
}
}
}
func EnvVarsContains(element EnvironmentVariable, array []EnvironmentVariable) bool {
for _, v := range array {
if v == element {
@@ -177,12 +281,16 @@ func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone ch
func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, _ := servicemap.IsServiceInRegion("ecs", r)
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getECSEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -192,32 +300,32 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getLambdaEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
// AppRunner is not supported in the aws service region catalog so we have to run it in all regions
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAppRunnerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
res, err = servicemap.IsServiceInRegion("lightsail", r)
res, err = servicemap.IsServiceInRegion("amazonlightsail.com", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getLightsailEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("sagemaker", r)
res, err = servicemap.IsServiceInRegion("sagemaker-ai", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getSagemakerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -226,8 +334,8 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -235,9 +343,9 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
//var PaginationMarker *string
@@ -256,11 +364,11 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
m.getECSEnvironmentVariablesPerDefinition(containerDefinition, region, dataReceiver)
m.getECSEnvironmentVariablesPerDefinition(*DescribeTaskDefinition.TaskDefinition, containerDefinition, region, dataReceiver)
}
}
}
@@ -282,7 +390,7 @@ func (m *EnvsModule) getTaskDefinitionFamilies(region string) []string {
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -299,12 +407,13 @@ func (m *EnvsModule) getTaskDefinitionFamilies(region string) []string {
}
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(containerDefinition ecsTypes.ContainerDefinition, region string, dataReceiver chan EnvironmentVariable) {
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(taskDefinition ecsTypes.TaskDefinition, containerDefinition ecsTypes.ContainerDefinition, region string, dataReceiver chan EnvironmentVariable) {
if containerDefinition.Environment != nil {
for _, x := range containerDefinition.Environment {
dataReceiver <- EnvironmentVariable{
service: "ECS",
taskDefinition: getNameFromARN(aws.ToString(taskDefinition.TaskDefinitionArn)),
name: aws.ToString(containerDefinition.Name),
region: region,
environmentVarName: aws.ToString(x.Name),
@@ -316,8 +425,8 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(containerDefinition
func (m *EnvsModule) getLambdaEnvironmentVariablesPerRegion(region string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -325,41 +434,21 @@ func (m *EnvsModule) getLambdaEnvironmentVariablesPerRegion(region string, wg *s
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
for {
functions, err := m.LambdaClient.ListFunctions(
context.TODO(),
&lambda.ListFunctionsInput{
Marker: PaginationMarker,
},
func(o *lambda.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, function := range functions.Functions {
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
}
// Pagination control. After the last page of output, the for loop exits.
if functions.NextMarker != nil {
PaginationMarker = functions.NextMarker
} else {
PaginationMarker = nil
break
}
Functions, err := sdk.CachedLambdaListFunctions(m.LambdaClient, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, function := range Functions {
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
}
}
func (m *EnvsModule) getLambdaEnvironmentVariablesPerFunction(function lambdaTypes.FunctionConfiguration, region string, dataReceiver chan EnvironmentVariable) {
@@ -378,8 +467,8 @@ func (m *EnvsModule) getLambdaEnvironmentVariablesPerFunction(function lambdaTyp
func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -387,80 +476,62 @@ func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *syn
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
for {
ListServices, err := m.AppRunnerClient.ListServices(
context.TODO(),
&(apprunner.ListServicesInput{
NextToken: PaginationControl,
}),
func(o *apprunner.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
if len(ListServices.ServiceSummaryList) > 0 {
ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r)
for _, service := range ListServices.ServiceSummaryList {
name := aws.ToString(service.ServiceName)
arn := aws.ToString(service.ServiceArn)
awsService := "App Runner"
if err != nil {
//modLog.Error(err.Error())
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
if len(ServiceSummaryList) > 0 {
DescribeService, err := m.AppRunnerClient.DescribeService(
context.TODO(),
&apprunner.DescribeServiceInput{
ServiceArn: &arn,
},
func(o *apprunner.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
for _, service := range ServiceSummaryList {
name := aws.ToString(service.ServiceName)
arn := aws.ToString(service.ServiceArn)
awsService := "App Runner"
}
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
//fmt.Printf("%s - %s", k, v)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
DescribeService, err := m.AppRunnerClient.DescribeService(
context.TODO(),
&apprunner.DescribeServiceInput{
ServiceArn: &arn,
},
func(o *apprunner.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
break
}
}
// The "NextToken" value is nil when there's no more data to return.
if ListServices.NextToken != nil {
PaginationControl = ListServices.NextToken
} else {
PaginationControl = nil
break
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
//fmt.Printf("%s - %s", k, v)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
}
}
}
func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -468,52 +539,43 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "Lightsail [Container]"
GetContainerServices, err := m.LightsailClient.GetContainerServices(
context.TODO(),
&(lightsail.GetContainerServicesInput{}),
func(o *lightsail.Options) {
o.Region = r
},
)
ContainerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
if err == nil {
if len(ContainerServices) > 0 {
if len(GetContainerServices.ContainerServices) > 0 {
for _, containerService := range GetContainerServices.ContainerServices {
for _, container := range containerService.CurrentDeployment.Containers {
for k, v := range container.Environment {
name := aws.ToString(containerService.ContainerServiceName)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
for _, containerService := range ContainerServices {
for _, container := range containerService.CurrentDeployment.Containers {
for k, v := range container.Environment {
name := aws.ToString(containerService.ContainerServiceName)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
}
}
}
func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -521,9 +583,9 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "Sagemaker"
var PaginationControl *string
@@ -541,7 +603,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -559,7 +621,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -603,7 +665,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -621,21 +683,19 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
if err == nil {
if len(DescribeTransformJob.Environment) > 0 {
name := fmt.Sprintf("[Transform Job] %s", aws.ToString(DescribeTransformJob.TransformJobName))
for k, v := range DescribeTransformJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
if len(DescribeTransformJob.Environment) > 0 {
name := fmt.Sprintf("[Transform Job] %s", aws.ToString(DescribeTransformJob.TransformJobName))
for k, v := range DescribeTransformJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
@@ -666,7 +726,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -684,23 +744,21 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
if err == nil {
if len(DescribeTrainingJob.Environment) > 0 {
name := fmt.Sprintf("[Training Job] %s", aws.ToString(DescribeTrainingJob.TrainingJobName))
for k, v := range DescribeTrainingJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
if len(DescribeTrainingJob.Environment) > 0 {
name := fmt.Sprintf("[Training Job] %s", aws.ToString(DescribeTrainingJob.TrainingJobName))
for k, v := range DescribeTrainingJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
}
@@ -730,7 +788,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -747,10 +805,13 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
if err == nil {
// find the number of environment variables in the model in a pointer safe way
if DescribeModel.PrimaryContainer != nil {
if len(DescribeModel.PrimaryContainer.Environment) > 0 {
name := fmt.Sprintf("[Model] %s", aws.ToString(DescribeModel.ModelName))
@@ -776,3 +837,22 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
}
}
func (m *EnvsModule) interestingEnvVarsOnly() [][]string {
var interestingBody [][]string
for _, envVar := range m.EnvironmentVariables {
if envVar.interesting {
interestingBody = append(
interestingBody, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
envVar.name,
magenta(envVar.environmentVarName),
magenta(envVar.environmentVarValue),
},
)
}
}
return interestingBody
}
+173 -120
View File
@@ -7,33 +7,36 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/efs"
"github.com/aws/aws-sdk-go-v2/service/efs/types"
"github.com/aws/aws-sdk-go-v2/service/fsx"
fsxTypes "github.com/aws/aws-sdk-go-v2/service/fsx/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
)
var green = color.New(color.FgGreen).SprintFunc()
type FilesystemsModule struct {
// General configuration data
EFSClient *efs.Client
FSxClient *fsx.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Filesystems []FilesystemObject
@@ -53,10 +56,11 @@ type FilesystemObject struct {
IP string
Policy string
MountTarget string
Permissions string
}
func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "filesystems"
@@ -66,10 +70,6 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
//populate region in the filesystems module struct
for i, region := range m.AWSRegions {
m.Regions[i].Region = region
}
fmt.Printf("[%s][%s] Enumerating filesystems for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: EFS, FSx \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -111,13 +111,50 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
})
m.output.Headers = []string{
"Service",
"Account",
"Region",
"Name",
"DNS Name",
//"IP",
"Mount Target",
"Policy",
"Permissions",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Name",
"DNS Name",
//"IP",
"Mount Target",
"Policy",
"Permissions",
}
} else {
tableCols = []string{
"Region",
"Name",
"DNS Name",
//"IP",
"Mount Target",
"Policy",
"Permissions",
}
}
// Table rows
@@ -125,24 +162,39 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
m.output.Body = append(
m.output.Body,
[]string{
m.Filesystems[i].AWSService,
aws.ToString(m.Caller.Account),
m.Filesystems[i].Region,
m.Filesystems[i].Name,
m.Filesystems[i].DnsName,
//m.Filesystems[i].IP,
m.Filesystems[i].MountTarget,
m.Filesystems[i].Policy,
m.Filesystems[i].Permissions,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath, verbosity)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s filesystems found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -167,8 +219,12 @@ func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDon
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("efs", r)
if err != nil {
@@ -187,8 +243,9 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
}
f := filepath.Join(path, "filesystems-mount-commands.txt")
@@ -198,8 +255,8 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
switch m.Filesystems[i].AWSService {
case "EFS":
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
out = out + fmt.Sprintf("mkdir -p /efs/%s/\n", m.Filesystems[i].MountTarget)
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs/%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
out = out + fmt.Sprintf("mkdir -p /efs%s/\n", m.Filesystems[i].MountTarget)
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
case "FSx [LUSTRE]":
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
out = out + fmt.Sprintln("#sudo amazon-linux-extras install -y lustre2.10")
@@ -224,8 +281,9 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(f, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
if verbosity > 2 {
@@ -239,117 +297,98 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
var PaginationMarker2 *string
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
var policy string
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
for {
DescribeFileSystems, err := m.EFSClient.DescribeFileSystems(
DescribeFileSystems, err := sdk.CachedDescribeFileSystems(m.EFSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, filesystem := range DescribeFileSystems {
name := filesystem.Name
id := aws.ToString(filesystem.FileSystemId)
_, err := m.EFSClient.DescribeFileSystemPolicy(
context.TODO(),
&efs.DescribeFileSystemsInput{
Marker: PaginationMarker,
&efs.DescribeFileSystemPolicyInput{
FileSystemId: filesystem.FileSystemId,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, filesystem := range DescribeFileSystems.FileSystems {
name := filesystem.Name
id := filesystem.FileSystemId
//dnsName := fmt.Sprintf("%s.efs.%s.amazonaws.com", *id, r)
_, err := m.EFSClient.DescribeFileSystemPolicy(
context.TODO(),
&efs.DescribeFileSystemPolicyInput{
FileSystemId: id,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
policy = "Default (No IAM auth)"
}
for {
DescribeMountTargets, err := m.EFSClient.DescribeMountTargets(
context.TODO(),
&efs.DescribeMountTargetsInput{
FileSystemId: id,
Marker: PaginationMarker2,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, mountTarget := range DescribeMountTargets.MountTargets {
mountTargetId := mountTarget.MountTargetId
ip := *mountTarget.IpAddress
awsService := "EFS"
dataReceiver <- FilesystemObject{
AWSService: awsService,
Region: r,
Name: aws.ToString(name),
DnsName: ip,
Policy: policy,
MountTarget: aws.ToString(mountTargetId),
}
}
if DescribeMountTargets.NextMarker != nil {
PaginationMarker2 = DescribeMountTargets.NextMarker
} else {
PaginationMarker2 = nil
break
}
}
//awsService := fmt.Sprintf("EFS [%s]", fsType)
}
// Pagination control. After the last page of output, the for loop exits.
if DescribeFileSystems.NextMarker != nil {
PaginationMarker = DescribeFileSystems.NextMarker
policy = "Default (No IAM auth)"
} else {
PaginationMarker = nil
policy = "IAM Authenticated"
}
DescribeMountTargets, err := sdk.CachedDescribeMountTargets(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
break
}
for _, mountTarget := range DescribeMountTargets {
ip := *mountTarget.IpAddress
mountTarget := aws.ToString(mountTarget.MountTargetId)
awsService := "EFS"
accessPoints, err := sdk.CachedDescribeAccessPoints(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
dataReceiver <- FilesystemObject{
AWSService: awsService,
Region: r,
Name: aws.ToString(name),
DnsName: ip,
Policy: policy,
MountTarget: mountTarget,
Permissions: "access denied",
}
}
for _, accessPoint := range accessPoints {
path, permissions := m.getEFSfilesystemPermissions(accessPoint)
dataReceiver <- FilesystemObject{
AWSService: awsService,
Region: r,
Name: aws.ToString(name),
DnsName: ip,
Policy: policy,
MountTarget: path,
Permissions: permissions,
}
}
}
}
}
func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
var PaginationMarker2 *string
@@ -369,8 +408,9 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
break
}
@@ -484,3 +524,16 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
}
}
func (m *FilesystemsModule) getEFSfilesystemPermissions(accessPoint types.AccessPointDescription) (string, string) {
var path string
var permissions string
if accessPoint.AccessPointId != nil && accessPoint.RootDirectory != nil && accessPoint.RootDirectory.CreationInfo != nil {
path = aws.ToString(accessPoint.RootDirectory.Path)
permissions = aws.ToString(accessPoint.RootDirectory.CreationInfo.Permissions)
return path, permissions
}
return "", ""
}
+418
View File
@@ -0,0 +1,418 @@
package aws
import (
"context"
"fmt"
"os"
"strings"
ingestor "github.com/BishopFox/cloudfox/aws/graph/ingester"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/knownawsaccountslookup"
"github.com/sirupsen/logrus"
)
type GraphCommand struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
AWSProfile string
WrapTable bool
AWSOutputType string
AWSTableCols string
Verbosity int
AWSOutputDirectory string
AWSConfig aws.Config
Version string
SkipAdminCheck bool
PmapperDataBasePath string
pmapperMod PmapperModule
pmapperError error
vendors *knownawsaccountslookup.Vendors
// Main module data
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
func (m *GraphCommand) RunGraphCommand() {
// These struct values are used by the output module
m.output.Verbosity = m.Verbosity
m.output.Directory = m.AWSOutputDirectory
m.output.CallingModule = "graph"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.vendors = knownawsaccountslookup.NewVendorMap()
m.vendors.PopulateKnownAWSAccounts()
m.modLog.Info("Collecting data for graph ingestor...")
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
////////////////
// Accounts
////////////////
accounts := m.collectAccountDataForGraph()
// write data to jsonl file for ingestor to read
fileName := fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "accounts")
// create file and directory if it doesn't exist
if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
m.modLog.Error(err)
return
}
outputFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
m.modLog.Error(err)
return
}
defer outputFile.Close()
for _, account := range accounts {
if err := internal.WriteJsonlFile(outputFile, account); err != nil {
m.modLog.Error(err)
return
}
}
////////////////
// Users
////////////////
// users := m.collectUserDataForGraph()
// // write data to jsonl file for ingestor to read
// fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "users")
// // create file and directory if it doesn't exist
// if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
// m.modLog.Error(err)
// return
// }
// outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
// if err != nil {
// m.modLog.Error(err)
// return
// }
// defer outputFile.Close()
// for _, user := range users {
// if err := internal.WriteJsonlFile(outputFile, user); err != nil {
// m.modLog.Error(err)
// return
// }
// }
////////////////
// Roles
////////////////
roles := m.collectRoleDataForGraph()
// write data to jsonl file for ingestor to read
fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "roles")
// create file and directory if it doesn't exist
if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
m.modLog.Error(err)
return
}
outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
m.modLog.Error(err)
return
}
defer outputFile.Close()
for _, role := range roles {
if err := internal.WriteJsonlFile(outputFile, role); err != nil {
m.modLog.Error(err)
return
}
}
ingestor, err := ingestor.NewCloudFoxIngestor()
if err != nil {
return
}
// Pmapper hack
goCtx := context.Background()
sharedLogger.Infof("Verifying connectivity to Neo4J at %s", ingestor.Uri)
if err := ingestor.Driver.VerifyConnectivity(goCtx); err != nil {
sharedLogger.Error(err)
}
defer ingestor.Driver.Close(goCtx)
m.pmapperMod.GenerateCypherStatements(goCtx, ingestor.Driver)
// back to regular stuff
ingestor.Run(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)))
}
func (m *GraphCommand) collectAccountDataForGraph() []models.Account {
//OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
var accounts []models.Account
OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(OrganizationsCommandClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Fatal(err)
}
if DescribeOrgOutput.MasterAccountId == nil {
m.modLog.Error("Organization is not configured")
}
// If the account is not the org mgmt account, it can only see some info about itself and some info about the org mgmt account.
// populate both of them here.
if aws.ToString(DescribeOrgOutput.MasterAccountId) != aws.ToString(m.Caller.Account) {
//create new object of type models.Account for this account
thisAccount := models.Account{
Id: aws.ToString(m.Caller.Account),
Arn: fmt.Sprintf("arn:aws:iam::%s:root", aws.ToString(m.Caller.Account)),
//Name: aws.ToString(m.Caller.Account),
IsOrgMgmt: false,
IsChildAccount: true,
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
}
accounts = append(accounts, thisAccount)
//create new object of type models.Account for the mgmt account
mgmtAccount := models.Account{
Id: aws.ToString(DescribeOrgOutput.MasterAccountId),
Arn: aws.ToString(DescribeOrgOutput.MasterAccountArn),
Email: aws.ToString(DescribeOrgOutput.MasterAccountEmail),
//Status: string(account.Status),
//JoinedMethod: string(account.JoinedMethod),
//JoinedTimestamp: account.JoinedTimestamp.String(),
IsOrgMgmt: true,
IsChildAccount: false,
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
}
accounts = append(accounts, mgmtAccount)
return accounts
} else {
// In this case we are the org mgmt account, so we can see all the accounts in the org.
ListAccounts, err := sdk.CachedOrganizationsListAccounts(OrganizationsCommandClient, aws.ToString(DescribeOrgOutput.MasterAccountId))
if err != nil {
m.modLog.Fatal(err)
}
for _, account := range ListAccounts {
var isMgmtAccount bool
var isChildAccount bool
m.modLog.Info("Account: ", aws.ToString(account.Name))
if aws.ToString(DescribeOrgOutput.MasterAccountId) == aws.ToString(account.Id) {
// this is the org mgmt account
isMgmtAccount = true
isChildAccount = false
} else if DescribeOrgOutput.MasterAccountId == nil {
// this is a standalone account
isChildAccount = false
isMgmtAccount = false
} else {
// this is a child account
isChildAccount = true
isMgmtAccount = false
}
//create new object of type models.Account
account := models.Account{
Id: aws.ToString(account.Id),
Arn: aws.ToString(account.Arn),
Name: aws.ToString(account.Name),
Email: aws.ToString(account.Email),
Status: string(account.Status),
JoinedMethod: string(account.JoinedMethod),
JoinedTimestamp: account.JoinedTimestamp.String(),
IsOrgMgmt: isMgmtAccount,
IsChildAccount: isChildAccount,
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
}
accounts = append(accounts, account)
}
return accounts
}
}
func (m *GraphCommand) collectRoleDataForGraph() []models.Role {
var isAdmin, canPrivEscToAdmin string
// iamClient := InitIAMClient(m.AWSConfig)
// iamSimClient := InitIamCommandClient(iamClient, m.Caller, m.AWSProfile, m.Goroutines)
// localAdminMap := make(map[string]bool)
var roles []models.Role
IAMCommandClient := InitIAMClient(m.AWSConfig)
ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err)
}
for _, role := range ListRolesOutput {
accountId := strings.Split(aws.ToString(role.Arn), ":")[4]
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
break
}
// if m.pmapperError == nil {
// isAdmin, canPrivEscToAdmin = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, role.Arn)
// } else {
// isAdmin, canPrivEscToAdmin = GetIamSimResult(m.SkipAdminCheck, role.Arn, iamSimClient, localAdminMap)
// }
// for _, row := range m.PermissionRowsFromAllProfiles {
// if row.Arn == aws.ToString(role.Arn) {
// // look for cases where there is a permission that allows sts:assumeRole or *
// // lowercase the action and compare it against the list of checks below
// if strings.EqualFold(row.Action, "sts:AssumeRole") ||
// strings.EqualFold(row.Action, "*") ||
// strings.EqualFold(row.Action, "sts:Assume*") ||
// strings.EqualFold(row.Action, "sts:*") {
// if row.Effect == "Allow" {
// //PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
// if row.Resource == "*" {
// PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
// } else if strings.EqualFold(row.Resource, aws.ToString(role.Arn)) {
// PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
// }
// }
// }
// if row.Effect == "Deny" {
// // Remove the string sts:AssumeRole from the PrivEscPermissions slice
// for i, v := range PrivEscPermissions {
// if v == "sts:AssumeRole" {
// PrivEscPermissions = append(PrivEscPermissions[:i], PrivEscPermissions[i+1:]...)
// }
// }
// }
// }
// }
var TrustedPrincipals []models.TrustedPrincipal
var TrustedServices []models.TrustedService
var TrustedFederatedProviders []models.TrustedFederatedProvider
//var TrustedFederatedSubjects string
var trustedProvider string
var trustedSubjects string
var vendorName string
for _, statement := range trustsdoc.Statement {
for _, principal := range statement.Principal.AWS {
if strings.Contains(principal, ":root") {
//check to see if the accountID is known
accountID := strings.Split(principal, ":")[4]
vendorName = m.vendors.GetVendorNameFromAccountID(accountID)
}
TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{
TrustedPrincipal: principal,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
VendorName: vendorName,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
})
}
for _, service := range statement.Principal.Service {
TrustedServices = append(TrustedServices, models.TrustedService{
TrustedService: service,
AccountID: accountId,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
})
}
for _, federated := range statement.Principal.Federated {
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
trustedProvider = "GitHub"
trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",")
if trustedSubjects == "" {
trustedSubjects = "ALL REPOS!!!"
} else {
trustedSubjects = "Repos: " + trustedSubjects
}
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")"
}
trustedSubjects = "Not applicable"
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != nil || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != nil {
trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")"
// if statement.Condition.StringEquals.OidcEksSub != "" {
// trustedSubjects = statement.Condition.StringEquals.OidcEksSub
// } else if statement.Condition.StringLike.OidcEksSub != "" {
// trustedSubjects = statement.Condition.StringLike.OidcEksSub
// } else {
// trustedSubjects = "ALL SERVICE ACCOUNTS!"
// }
trustedSubjects = "ALL SERVICE ACCOUNTS!"
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")"
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR
}
} else {
if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")"
trustedSubjects = "ALL SERVICE ACCOUNTS!"
} else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
}
trustedSubjects = "Not applicable"
}
TrustedFederatedProviders = append(TrustedFederatedProviders, models.TrustedFederatedProvider{
TrustedFederatedProvider: federated,
ProviderShortName: trustedProvider,
TrustedSubjects: trustedSubjects,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
})
}
}
//create new object of type models.Role
role := models.Role{
Id: aws.ToString(role.Arn),
AccountID: accountId,
ARN: aws.ToString(role.Arn),
Name: aws.ToString(role.RoleName),
TrustsDoc: trustsdoc,
TrustedPrincipals: TrustedPrincipals,
TrustedServices: TrustedServices,
TrustedFederatedProviders: TrustedFederatedProviders,
CanPrivEscToAdmin: canPrivEscToAdmin,
IsAdmin: isAdmin,
}
roles = append(roles, role)
}
return roles
}
+1
View File
@@ -0,0 +1 @@
package collectors
+283
View File
@@ -0,0 +1,283 @@
package ingestor
import (
"bufio"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
log "github.com/sirupsen/logrus"
)
const (
// Neo4j
MergeNodeQueryTemplate = `CALL apoc.merge.node([$labels[0]], {Id: $Id}, $properties, $properties) YIELD node as obj
CALL apoc.create.setLabels(obj, $labels) YIELD node as labeledObj
RETURN labeledObj`
MergeRelationQueryTemplate = `UNWIND $batch as row
CALL apoc.merge.node([row.sourceLabel], apoc.map.fromValues([row.sourceProperty, row.sourceNodeId])) YIELD node as from
CALL apoc.merge.node([row.targetLabel], apoc.map.fromValues([row.targetProperty, row.targetNodeId])) YIELD node as to
CALL apoc.merge.relationship(from, row.relationshipType, {}, row.properties, to) YIELD rel
RETURN rel`
// Using sprintf to insert the label name since the driver doesn't support parameters for labels here
// %[1]s is a nice way to say "insert the first parameter here"
CreateConstraintQueryTemplate = "CREATE CONSTRAINT IF NOT EXISTS FOR (n: %s) REQUIRE n.Id IS UNIQUE"
CreateIndexQueryTemplate = "CREATE INDEX %[1]s_Id IF NOT EXISTS FOR (n: %[1]s) ON (n.Id)"
PostProcessMergeQueryTemplate = `MATCH (n)
WITH n.Id AS Id, COLLECT(n) AS nodesToMerge
WHERE size(nodesToMerge) > 1
CALL apoc.refactor.mergeNodes(nodesToMerge, {properties: 'combine', mergeRels:true})
YIELD node
RETURN count(*);`
)
type Neo4jConfig struct {
Uri string
Username string
Password string
}
type CloudFoxIngestor struct {
Neo4jConfig
//ResultsFile string
Driver neo4j.DriverWithContext
TmpDir string
}
func NewCloudFoxIngestor() (*CloudFoxIngestor, error) {
config := Neo4jConfig{
Uri: "neo4j://localhost:7687",
Username: "neo4j",
Password: "cloudfox",
}
driver, err := neo4j.NewDriverWithContext(config.Uri, neo4j.BasicAuth(config.Username, config.Password, ""))
if err != nil {
return nil, err
}
return &CloudFoxIngestor{
Neo4jConfig: config,
//ResultsFile: resultsFile,
Driver: driver,
}, nil
}
// func unzipToTemp(zipFilePath string) (string, error) {
// // Create a temporary directory to extract the zip file to
// tempDir, err := os.MkdirTemp("", "cloudfox-graph")
// if err != nil {
// return "", err
// }
// // Open the zip file and extract to a temporary directory
// zipfile, err := zip.OpenReader(zipFilePath)
// if err != nil {
// return "", err
// }
// defer zipfile.Close()
// for _, file := range zipfile.File {
// path := filepath.Join(tempDir, file.Name)
// log.Debugf("Extracting file: %s", path)
// fileData, err := file.Open()
// if err != nil {
// return "", err
// }
// defer fileData.Close()
// newFile, err := os.Create(path)
// if err != nil {
// return "", err
// }
// defer newFile.Close()
// if _, err := io.Copy(newFile, fileData); err != nil {
// return "", err
// }
// }
// return tempDir, nil
// }
func (i *CloudFoxIngestor) ProcessFile(path string, info os.FileInfo) error {
log.Infof("Processing file: %s", info.Name())
switch info.Name() {
case "accounts.jsonl":
return i.ProcessFileObjects(path, schema.Account, schema.Account)
case "roles.jsonl":
return i.ProcessFileObjects(path, schema.Role, schema.Role)
// case "servicePrincipals.jsonl":
// return i.ProcessFileObjects(path, schema.GraphServicePrincipal, schema.GraphObject)
// case "applications.jsonl":
// return i.ProcessFileObjects(path, schema.GraphApplication, schema.GraphObject)
// case "devices.jsonl":
// return i.ProcessFileObjects(path, schema.GraphDevice, schema.GraphObject)
// case "directoryRoles.jsonl":
// return i.ProcessFileObjects(path, schema.GraphRole, schema.GraphObject)
// case "subscriptions.jsonl":
// return i.ProcessFileObjects(path, schema.Subscription, schema.ArmResource)
// case "tenants.jsonl":
// return i.ProcessFileObjects(path, schema.Tenant, schema.ArmResource)
// case "rbac.jsonl":
// return i.ProcessFileObjects(path, schema.AzureRbac, "")
default:
return nil
}
}
func (i *CloudFoxIngestor) ProcessFileObjects(path string, objectType schema.NodeLabel, generalType schema.NodeLabel) error {
var object = models.NodeLabelToNodeMap[objectType]
// Open the file
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// Read the file line by line
scanner := bufio.NewScanner(file)
//Iterate over the lines and create the nodes
for scanner.Scan() {
line := strings.TrimSuffix(scanner.Text(), "\n")
// Skip empty lines
if len(line) > 0 {
if err := json.Unmarshal([]byte(line), &object); err != nil {
log.Errorf("%s : %s", err, line)
continue
}
}
relationships := object.MakeRelationships()
if err := i.InsertDBObjects(object, relationships, []schema.NodeLabel{generalType, objectType}); err != nil {
log.Error(err)
continue
}
}
return nil
}
func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []schema.Relationship, labels []schema.NodeLabel) error {
goCtx := context.Background()
var err error
// Insert the node
if object != nil {
nodeMap, err := schema.ConvertCustomTypesToNeo4j(&object)
if err != nil {
log.Errorf("Error converting custom types to neo4j: %s -- %v", err, object)
return err
}
//nodeMap := schema.AsNeo4j(&object)
nodeQueryParams := map[string]interface{}{
"Id": nodeMap["Id"],
"labels": labels,
"properties": nodeMap,
}
_, err = neo4j.ExecuteQuery(goCtx, i.Driver, MergeNodeQueryTemplate, nodeQueryParams, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
log.Errorf("Error inserting node: %s -- %v", err, nodeQueryParams)
return err
}
}
// Insert the relationships
if len(relationships) > 0 {
var relationshipInterface []map[string]interface{}
// Check the default SourceProperty and TargetProperty values
for _, relationship := range relationships {
var currentRelationship map[string]interface{}
if relationship.SourceProperty == "" {
relationship.SourceProperty = "Id"
}
if relationship.TargetProperty == "" {
relationship.TargetProperty = "Id"
}
relationshipBytes, err := json.Marshal(relationship)
if err != nil {
return err
}
if err := json.Unmarshal(relationshipBytes, &currentRelationship); err != nil {
return err
}
relationshipInterface = append(relationshipInterface, currentRelationship)
}
_, err = neo4j.ExecuteQuery(goCtx, i.Driver, MergeRelationQueryTemplate, map[string]interface{}{"batch": relationshipInterface}, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
log.Errorf("Error inserting relationships: %s -- %v", err, relationshipInterface)
return err
}
}
return nil
}
func (i *CloudFoxIngestor) Run(graphOutputDir string) error {
goCtx := context.Background()
log.Infof("Verifying connectivity to Neo4J at %s", i.Uri)
if err := i.Driver.VerifyConnectivity(goCtx); err != nil {
return err
}
defer i.Driver.Close(goCtx)
var err error
// Get the label to model map
// Create constraints and indexes
// log.Info("Creating constraints and indexes for labels")
// for label := range models.NodeLabelToNodeMap {
// for _, query := range []string{CreateConstraintQueryTemplate, CreateIndexQueryTemplate} {
// _, err := neo4j.ExecuteQuery(goCtx, i.Driver, fmt.Sprintf(query, label), nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
// if err != nil {
// log.Error(err)
// continue
// }
// }
// }
// Process the files in the output directory
fileWg := new(sync.WaitGroup)
filepath.Walk(graphOutputDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if graphOutputDir == path {
return nil
}
fileWg.Add(1)
go func(path string) {
defer fileWg.Done()
i.ProcessFile(path, info)
log.Infof("Finished processing file: %s", info.Name())
}(path)
return nil
})
fileWg.Wait()
log.Info("Finished processing files")
// Run the post processing merge query
log.Info("Running post processing merge query")
_, err = neo4j.ExecuteQuery(goCtx, i.Driver, PostProcessMergeQueryTemplate, nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
log.Error(err)
return err
}
return nil
}
@@ -0,0 +1,44 @@
package models
import "github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
type Account struct {
Id string
Arn string
Email string
Name string
Status string
JoinedMethod string
JoinedTimestamp string
IsOrgMgmt bool
IsChildAccount bool
OrgMgmtAccountID string
OrganizationID string
}
func (a *Account) MakeRelationships() []schema.Relationship {
var relationships []schema.Relationship
// make relationship from children accounts to parent org
if a.IsChildAccount {
// make relationship from child to org mgmt account
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.OrganizationID,
TargetNodeID: a.Id,
SourceLabel: schema.Organization,
TargetLabel: schema.Account,
RelationshipType: schema.Manages,
})
// make relationship from parent org mgmt account to child account
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: a.OrganizationID,
SourceLabel: schema.Account,
TargetLabel: schema.Organization,
RelationshipType: schema.MemberOf,
})
}
return relationships
}
@@ -0,0 +1,11 @@
package models
import (
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
)
var NodeLabelToNodeMap = map[schema.NodeLabel]schema.Node{
schema.Organization: &Organization{},
schema.Account: &Account{},
schema.Role: &Role{},
}
+20
View File
@@ -0,0 +1,20 @@
package models
import (
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
)
type Organization struct {
Id string
OrgId string
Arn string
MasterAccountArn string
MasterAccountId string
MasterAccountEmail string
ChildAccounts []Account
MgmtAccount Account
}
func (o *Organization) MakeRelationships() []schema.Relationship {
return []schema.Relationship{}
}
@@ -0,0 +1,5 @@
package models
type Resource struct {
ARN string
}
+647
View File
@@ -0,0 +1,647 @@
package models
import (
"fmt"
"strings"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/BishopFox/cloudfox/internal/common"
"github.com/dominikbraun/graph"
)
type Role struct {
Id string
AccountID string
ARN string
Name string
TrustsDoc policy.TrustPolicyDocument
TrustedPrincipals []TrustedPrincipal
TrustedServices []TrustedService
TrustedFederatedProviders []TrustedFederatedProvider
CanPrivEscToAdmin string
IsAdmin string
IdValue string
IsAdminP bool
PathToAdminSameAccount bool
PathToAdminCrossAccount bool
}
type TrustedPrincipal struct {
TrustedPrincipal string
ExternalID string
VendorName string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedService struct {
TrustedService string
AccountID string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedFederatedProvider struct {
TrustedFederatedProvider string
ProviderShortName string
TrustedSubjects string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
func (a *Role) MakeRelationships() []schema.Relationship {
var relationships []schema.Relationship
//instance := singleton.GetInstance()
// get thisAccount id from role arn
var thisAccount string
if len(a.ARN) >= 25 {
thisAccount = a.ARN[13:25]
} else {
fmt.Sprintf("Could not get account number from this role arn%s", a.ARN)
}
// make a relationship between each role and the account it belongs to
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: thisAccount,
SourceLabel: schema.Role,
TargetLabel: schema.Account,
RelationshipType: schema.MemberOf,
})
for _, TrustedPrincipal := range a.TrustedPrincipals {
//get account id from the trusted principal arn
var trustedPrincipalAccount string
if len(TrustedPrincipal.TrustedPrincipal) >= 25 {
trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25]
} else {
fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal)
}
var PermissionsRowAccount string
// make a TRUSTED_BY relationship between the role and the trusted principal. This does not mean the principal can assume this role, we need more logic to determine that (see below)
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: TrustedPrincipal.TrustedPrincipal,
// TargetNodeID: a.Id,
// SourceLabel: schema.Principal,
// TargetLabel: schema.Role,
// RelationshipType: schema.IsTrustedBy,
// })
// // make a TRUSTS relationship between the trusted principal and this role. This does not mean the principal can assume this role, we need more logic to determine that (see below)
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: TrustedPrincipal.TrustedPrincipal,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.Trusts,
// })
// // make a MEMBER_OF relationship between the role and the account
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: TrustedPrincipal.TrustedPrincipal,
// TargetNodeID: trustedPrincipalAccount,
// SourceLabel: schema.Principal,
// TargetLabel: schema.Account,
// RelationshipType: schema.MemberOf,
// })
// if the role trusts a principal in this same account explicitly, then the principal can assume the role
if thisAccount == trustedPrincipalAccount {
// make a CAN_ASSUME relationship between the trusted principal and this role
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
TargetNodeID: a.Id,
SourceLabel: schema.Principal,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
// make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: TrustedPrincipal.TrustedPrincipal,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.CanBeAssumedBy,
// })
}
// If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role
// if the role we are looking at trusts root in it's own account
if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) {
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == thisAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
//evaluate if the princiapl is a user or a role and set a variable accordingly
//var principalType schema.NodeLabel
if strings.EqualFold(PermissionsRow.Type, "User") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.User,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.Role,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
}
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: PermissionsRow.Arn,
// TargetNodeID: a.Id,
// SourceLabel: principalType,
// TargetLabel: schema.Role,
// RelationshipType: schema.CanAssumeTest,
// })
// make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: PermissionsRow.Arn,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.CanBeAssumedByTest,
// })
}
}
}
}
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) {
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this other account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == trustedPrincipalAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
if strings.EqualFold(PermissionsRow.Type, "User") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.User,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssumeCrossAccount,
})
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.User,
TargetLabel: schema.Role,
RelationshipType: schema.CanAccess,
})
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.Role,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssumeCrossAccount,
})
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.Role,
TargetLabel: schema.Role,
RelationshipType: schema.CanAccess,
})
}
// // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: PermissionsRow.Arn,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.CanBeAssumedByTest,
// })
}
}
}
}
}
// If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
TargetNodeID: a.Id,
SourceLabel: schema.Vendor,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
}
}
for _, TrustedService := range a.TrustedServices {
// make relationship from trusted service to this role of type can assume
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID,
TargetNodeID: a.Id,
SourceLabel: schema.Service,
TargetLabel: schema.Role,
RelationshipType: schema.IsTrustedBy,
})
// make relationship from this role to trusted service of type can be assumed by
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID,
SourceLabel: schema.Role,
TargetLabel: schema.Service,
RelationshipType: schema.Trusts,
})
}
for _, TrustedFederatedProvider := range a.TrustedFederatedProviders {
// make relationship from trusted federated provider to this role of type can assume
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
TargetNodeID: a.Id,
SourceLabel: schema.FederatedIdentity,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
// make relationship from this role to trusted federated provider of type can be assumed by
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
SourceLabel: schema.Role,
TargetLabel: schema.FederatedIdentity,
RelationshipType: schema.CanBeAssumedBy,
})
// make relationship from trusted federated provider to this role of type can assume
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
TargetNodeID: a.Id,
SourceLabel: schema.FederatedIdentity,
TargetLabel: schema.Role,
RelationshipType: schema.IsTrustedBy,
})
// make relationship from this role to trusted federated provider of type can be assumed by
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
SourceLabel: schema.Role,
TargetLabel: schema.FederatedIdentity,
RelationshipType: schema.Trusts,
})
}
return relationships
}
// func (a *Node) GenerateAttributes() map[string]string {
// attributes := make(map[string]string)
// attributes["Id"] = a.Id
// attributes["Name"] = a.Name
// attributes["Type"] = "Role"
// attributes["AccountID"] = a.AccountID
// attributes["ARN"] = a.ARN
// attributes["CanPrivEscToAdmin"] = a.CanPrivEscToAdmin
// attributes["IsAdmin"] = a.IsAdmin
// attributes["IdValue"] = a.IdValue
// return attributes
// }
// func (a *Role) MergeAttributes(newAttributes map[string]string) {
// if a.Id == "" {
// a.Id = newAttributes["Id"]
// }
// if a.Name == "" {
// a.Name = newAttributes["Name"]
// }
// if a.Type == "" {
// a.Type = newAttributes["Type"]
// }
// if a.AccountID == "" {
// a.AccountID = newAttributes["AccountID"]
// }
// if a.ARN == "" {
// a.ARN = newAttributes["ARN"]
// }
// if a.CanPrivEscToAdmin == "" {
// a.CanPrivEscToAdmin = newAttributes["CanPrivEscToAdmin"]
// }
// if a.IsAdmin == "" {
// a.IsAdmin = newAttributes["IsAdmin"]
// }
// if a.IdValue == "" {
// a.IdValue = newAttributes["IdValue"]
// }
// }
func (a *Role) MakeVertices(GlobalGraph graph.Graph[string, string]) {
// make a vertex for this role as populate all of the data in the Role struct as attributes
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Role"),
graph.VertexAttribute("AccountID", a.AccountID),
graph.VertexAttribute("ARN", a.ARN),
graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin),
graph.VertexAttribute("IsAdmin", a.IsAdmin),
graph.VertexAttribute("IdValue", a.IdValue),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
}
// func MakeAllVertices(GlobalRoles []Role, GlobalPmapperGraph aws.PmapperModule) (GlobalGraph graph.Graph[string, string]) {
// // for all nodes in the GlobalPmapperGraph, check to see if they exist in the GlobalRoles slice. If they do, then update the node with the new data. If they don't, then add the node to the GlobalRoles slice
// for _, node := range GlobalPmapperGraph.Nodes {
// // check to see if the node exists in the GlobalRoles slice
// var nodeExists bool
// for _, role := range GlobalRoles {
// if node.Id == role.Id {
// nodeExists = true
// }
// }
// // // make a vertex for this role as populate all of the data in the Role struct as attributes
// // err := GlobalGraph.AddVertex(
// // a.Id,
// // graph.VertexAttribute("Name", a.Name),
// // graph.VertexAttribute("Type", "Role"),
// // graph.VertexAttribute("AccountID", a.AccountID),
// // graph.VertexAttribute("ARN", a.ARN),
// // graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin),
// // graph.VertexAttribute("IsAdmin", a.IsAdmin),
// // graph.VertexAttribute("IdValue", a.IdValue),
// // )
// // if err != nil {
// // if err == graph.ErrVertexAlreadyExists {
// // fmt.Println(a.Id + " already exists")
// // }
// // }
// }
func (a *Role) MakeEdges(GlobalGraph graph.Graph[string, string]) []schema.Relationship {
var relationships []schema.Relationship
// get thisAccount id from role arn
var thisAccount string
if len(a.ARN) >= 25 {
thisAccount = a.ARN[13:25]
} else {
fmt.Sprintf("Could not get account number from this role arn%s", a.ARN)
}
for _, TrustedPrincipal := range a.TrustedPrincipals {
//get account id from the trusted principal arn
var trustedPrincipalAccount string
if len(TrustedPrincipal.TrustedPrincipal) >= 25 {
trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25]
} else {
fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal)
}
var PermissionsRowAccount string
// if the role trusts a principal in this same account explicitly, then the principal can assume the role
if thisAccount == trustedPrincipalAccount {
// make a CAN_ASSUME relationship between the trusted principal and this role
err := GlobalGraph.AddEdge(
TrustedPrincipal.TrustedPrincipal,
a.Id,
graph.EdgeAttribute("AssumeRole", "Same account explicit trust"),
)
if err != nil {
fmt.Println(err)
fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Id + "Same account explicit trust")
}
}
// If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role
// if the role we are looking at trusts root in it's own account
if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) {
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Account"),
graph.VertexAttribute("AccountID", a.AccountID),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == thisAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
//evaluate if the princiapl is a user or a role and set a variable accordingly
//var principalType schema.NodeLabel
if strings.EqualFold(PermissionsRow.Type, "User") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role")
}
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role")
}
}
}
}
}
}
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Account"),
graph.VertexAttribute("AccountID", a.AccountID),
graph.VertexAttribute("VendorName", TrustedPrincipal.VendorName),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
// If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
TargetNodeID: a.Id,
SourceLabel: schema.Vendor,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
err = GlobalGraph.AddEdge(
//TrustedPrincipal.TrustedPrincipal,
TrustedPrincipal.VendorName,
a.Id,
graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"),
)
if err != nil {
fmt.Println(err)
fmt.Println(TrustedPrincipal.VendorName + a.Id + "Cross account root trust and trusted principal is a vendor")
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) {
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Account"),
graph.VertexAttribute("AccountID", a.AccountID),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this other account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == trustedPrincipalAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
if strings.EqualFold(PermissionsRow.Type, "User") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role")
}
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role")
}
}
}
}
}
}
}
}
}
// pmapper takes care of this part so commenting out for now - but leaving as a placeholder
// for _, TrustedService := range a.TrustedServices {
// // make relationship from trusted service to this role of type can assume
// // make relationship from this role to trusted service of type can be assumed by
// }
for _, TrustedFederatedProvider := range a.TrustedFederatedProviders {
// make relationship from trusted federated provider to this role of type can assume
GlobalGraph.AddVertex(
TrustedFederatedProvider.TrustedFederatedProvider,
graph.VertexAttribute("Type", "FederatedIdentity"),
)
err := GlobalGraph.AddEdge(
TrustedFederatedProvider.TrustedFederatedProvider,
a.Id,
graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"),
)
if err != nil {
fmt.Println(err)
fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Id + "Trusted federated provider")
}
}
return relationships
}
+71
View File
@@ -0,0 +1,71 @@
package models
import (
"fmt"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
)
type User struct {
Id string
ARN string
Name string
IsAdmin string
CanPrivEscToAdmin string
IdValue string
}
func (a *User) MakeRelationships() []schema.Relationship {
// make a relationship between each role and the account it belongs to
relationships := []schema.Relationship{}
// get thisAccount id from user arn
var thisAccount string
if len(a.ARN) >= 25 {
thisAccount = a.ARN[13:25]
} else {
fmt.Sprintf("Could not get account number from this user arn%s", a.ARN)
}
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: thisAccount,
SourceLabel: schema.User,
TargetLabel: schema.Account,
RelationshipType: schema.MemberOf,
Properties: map[string]interface{}{},
})
return relationships
}
func (a *User) GenerateAttributes() map[string]string {
return map[string]string{
"Id": a.Id,
"ARN": a.ARN,
"Name": a.Name,
"IsAdmin": a.IsAdmin,
"CanPrivEscToAdmin": a.CanPrivEscToAdmin,
"IdValue": a.IdValue,
}
}
func (a *User) MergeAttributes(other map[string]string) {
if other["Id"] != "" {
a.Id = other["Id"]
}
if other["ARN"] != "" {
a.ARN = other["ARN"]
}
if other["Name"] != "" {
a.Name = other["Name"]
}
if other["IsAdmin"] != "" {
a.IsAdmin = other["IsAdmin"]
}
if other["CanPrivEscToAdmin"] != "" {
a.CanPrivEscToAdmin = other["CanPrivEscToAdmin"]
}
if other["IdValue"] != "" {
a.IdValue = other["IdValue"]
}
}
+148
View File
@@ -0,0 +1,148 @@
package schema
import (
"fmt"
"reflect"
"github.com/goccy/go-json"
"golang.org/x/exp/slices"
)
type RelationshipType string
type NodeLabel string
type Node interface {
MakeRelationships() []Relationship
}
type Relationship struct {
SourceNodeID string `json:"sourceNodeId"`
TargetNodeID string `json:"targetNodeId"`
SourceLabel NodeLabel `json:"sourceLabel"`
TargetLabel NodeLabel `json:"targetLabel"`
RelationshipType RelationshipType `json:"relationshipType"`
Properties map[string]interface{} `json:"properties"`
SourceProperty string `json:"sourceProperty"`
TargetProperty string `json:"targetProperty"`
}
const (
// Relationships
AssociatedTo RelationshipType = "AssociatedTo"
AttachedTo RelationshipType = "AttachedTo"
Authenticates RelationshipType = "Authenticates"
ConnectedTo RelationshipType = "ConnectedTo"
Contains RelationshipType = "Contains"
Exposes RelationshipType = "Exposes"
HasAccess RelationshipType = "HasAccess"
HasConfig RelationshipType = "HasConfig"
HasDisk RelationshipType = "HasDisk"
HasInstance RelationshipType = "HasInstance"
HasRbac RelationshipType = "HasRbac"
HasRole RelationshipType = "HasRole"
Manages RelationshipType = "Manages"
MemberOf RelationshipType = "MemberOf"
Owns RelationshipType = "Owns"
Represents RelationshipType = "Represents"
Trusts RelationshipType = "Trusts"
IsTrustedBy RelationshipType = "IsTrustedBy"
CanAssume RelationshipType = "CanAssume"
CanAssumeCrossAccount RelationshipType = "CanAssumeCrossAccount"
CanBeAssumedBy RelationshipType = "CanBeAssumedBy"
CanBeAssumedByTest RelationshipType = "CanBeAssumedByTest"
CanAssumeTest RelationshipType = "CanAssumeTest"
CanAccess RelationshipType = "CAN_ACCESS"
)
const (
// Node labels
Resource NodeLabel = "Resource"
Account NodeLabel = "Account"
Organization NodeLabel = "Org"
Service NodeLabel = "Service"
Principal NodeLabel = "Principal"
Vendor NodeLabel = "Vendor"
Role NodeLabel = "Role"
Group NodeLabel = "Group"
User NodeLabel = "User"
FederatedIdentity NodeLabel = "FederatedIdentity"
)
func AsNeo4j(object *Node) map[string]interface{} {
// We don't want to include these fields in the map
fieldsToExclude := []string{"members", "owners", "appRoles", "registeredUsers"}
objectMap, err := json.Marshal(object)
if err != nil {
return nil
}
var objectMapInterface map[string]interface{}
json.Unmarshal(objectMap, &objectMapInterface)
for _, field := range fieldsToExclude {
delete(objectMapInterface, field)
}
// We need to convert flatten maps to an array
// We'll want to keep order of the keys for things like extensionAttributes
for key, value := range objectMapInterface {
_, isMap := value.(map[string]interface{})
if isMap {
var valueArray []string
var keys []string
for k := range value.(map[string]interface{}) {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
valueString := value.(map[string]interface{})[k].(string)
if valueString != "" {
valueArray = append(valueArray, k, valueString)
}
}
objectMapInterface[key] = valueArray
}
}
return objectMapInterface
}
func ConvertCustomTypesToNeo4j(node interface{}) (map[string]interface{}, error) {
result := make(map[string]interface{})
val := reflect.ValueOf(node)
// Handling pointers to structs or interfaces
for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
val = val.Elem()
}
// Check if the value is valid and if it's a struct
if !val.IsValid() || val.Kind() != reflect.Struct {
return nil, fmt.Errorf("invalid input: not a struct or a pointer to a struct")
}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := val.Type().Field(i) // Get the StructField
// Check if the field is a struct or slice of structs and not one of the basic types
if (fieldType.Type.Kind() == reflect.Struct ||
(fieldType.Type.Kind() == reflect.Slice && fieldType.Type.Elem().Kind() == reflect.Struct)) &&
fieldType.Type != reflect.TypeOf([]string{}) &&
fieldType.Type != reflect.TypeOf([]int{}) &&
fieldType.Type != reflect.TypeOf([]bool{}) {
// Convert complex field to JSON string
jsonStr, err := json.Marshal(field.Interface())
if err != nil {
return nil, err
}
result[fieldType.Name] = string(jsonStr)
} else {
// Directly use the field for primitive types
result[fieldType.Name] = field.Interface()
}
}
return result, nil
}
+228 -189
View File
@@ -7,9 +7,11 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
@@ -19,20 +21,21 @@ import (
type IamSimulatorModule struct {
// General configuration data
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
IAMListUsersClient iam.ListUsersAPIClient
IAMListRolesClient iam.ListRolesAPIClient
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Goroutines int
AWSProfileProvided string
AWSProfileStub string
WrapTable bool
// Main module data
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
IamSimulatorAdminCheckOnly bool
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
@@ -45,44 +48,46 @@ type SimulatorResult struct {
Decision string
}
var (
defaultActionNames = []string{
"sts:AssumeRole",
"iam:PassRole",
"secretsmanager:GetSecretValue",
"ssm:GetParameter",
"s3:ListBucket",
"s3:GetObject",
"ssm:sSendCommand",
"ssm:StartSession",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"eks:UpdateClusterConfig",
"lambda:ListFunctions",
"ecs:DescribeTaskDefinition",
"apprunner:DescribeService",
"ec2:DescribeInstanceAttributeInput",
}
TxtLogger = internal.TxtLogger()
)
var defaultActionNames = []string{
"sts:AssumeRole",
"iam:PassRole",
"secretsmanager:GetSecretValue",
"ssm:GetParameter",
"s3:ListBucket",
"s3:GetObject",
"ssm:SendCommand",
"ssm:StartSession",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"eks:UpdateClusterConfig",
"lambda:ListFunctions",
"ec2:DescribeInstanceAttributeInput",
"sns:Subscribe",
"sqs:SendMessage",
}
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputFormat string, outputDirectory string, verbosity int) {
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "iam-simulator"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
var filename string
var actionList []string
var pmapperCommands []string
var pmapperOutFileName string
var inputArn string
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
if m.AWSProfileProvided == "" {
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
} else {
m.AWSProfileStub = m.AWSProfileProvided
}
wg := new(sync.WaitGroup)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
@@ -97,41 +102,78 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
go m.Receiver(dataReceiver, receiverDone)
if m.IamSimulatorAdminCheckOnly {
// set defaultActionNames to an empty slice
defaultActionNames = []string{}
}
// This double if/else section is here to handle the cases where --principal or --action (or both) are specified.
if principal != "" {
if action != "" {
// The user specified a specific --principal and a specific --action
fmt.Printf("[%s][%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), principal, action)
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
fmt.Printf("[%s][%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal, action)
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
actionList = append(actionList, action)
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
if !strings.Contains(principal, "arn:") {
// try as a role
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), actionList, resource, dataReceiver)
// try as a user
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), actionList, resource, dataReceiver)
} else {
// the arn was supplied so just run it
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
}
} else {
// The user specified a specific --principal, but --action was empty
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), principal)
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
m.getPolicySimulatorResult((&principal), defaultActionNames, resource, dataReceiver)
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal)
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
if !strings.Contains(principal, "arn:") {
// try as a role
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), defaultActionNames, resource, dataReceiver)
// try as a user
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), defaultActionNames, resource, dataReceiver)
} else {
// the arn was supplied so just run it
m.getPolicySimulatorResult((&principal), defaultActionNames, resource, dataReceiver)
}
}
} else {
if action != "" {
// The did not specify a specific --principal, but they did specify an --action
fmt.Printf("[%s][%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), action)
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
fmt.Printf("[%s][%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), action)
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
actionList = append(actionList, action)
wg.Add(1)
m.getIAMUsers(wg, actionList, resource, dataReceiver)
wg.Add(1)
m.getIAMRoles(wg, actionList, resource, dataReceiver)
pmapperOutFileName = filepath.Join(m.output.FullFilename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
if m.AWSProfileProvided != "" {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
} else {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
}
} else {
// Both --principal and --action are empty. Run in default mode!
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.output.FullFilename = m.output.CallingModule
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
filename = m.output.CallingModule
m.executeChecks(wg, resource, dataReceiver)
for _, action := range defaultActionNames {
pmapperOutFileName = filepath.Join(m.output.FullFilename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
if m.AWSProfileProvided != "" {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
} else {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
}
}
}
@@ -151,11 +193,36 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
// Regardless of what options were selected, for now at least, we will always print the data using the output module (table/csv mode)
m.output.Headers = []string{
"Service",
"Account",
"Principal",
"Query",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Principal",
"Query",
}
} else {
tableCols = []string{
"Principal",
"Query",
}
}
sort.Slice(m.SimulatorResults, func(i, j int) bool {
return m.SimulatorResults[i].Query < m.SimulatorResults[j].Query
})
@@ -165,7 +232,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
m.output.Body = append(
m.output.Body,
[]string{
m.SimulatorResults[i].AWSService,
aws.ToString(m.Caller.Account),
m.SimulatorResults[i].Principal,
m.SimulatorResults[i].Query,
},
@@ -173,20 +240,35 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
fmt.Printf("[%s][%s] We suggest running the pmapper commands in the loot file to get the same information but taking privesc paths into account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: filename,
})
o.PrefixIdentifier = m.AWSProfileStub
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
fmt.Printf("[%s][%s] We suggest running the pmapper commands in the loot file to get the same information but taking privesc paths into account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
// fmt.Printf("[%s]\t\tpmapper --profile %s graph create\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
// for _, line := range pmapperCommands {
// fmt.Printf("[%s]\t\t%s", cyan(m.output.CallingModule), cyan(m.AWSProfile), line)
// }
m.writeLoot(m.output.FilePath, verbosity, pmapperCommands)
m.writeLoot(o.Table.DirectoryName, verbosity, pmapperCommands)
} else if principal != "" || action != "" {
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), m.output.CallingModule)
}
func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pmapperCommands []string) {
@@ -194,29 +276,33 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
outFile := filepath.Join(path, "iam-simulator-pmapper-commands.txt")
var out string
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfile)
if m.AWSProfileProvided != "" {
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfileProvided)
} else {
out = out + fmt.Sprintf("pmapper graph create\n")
}
for _, line := range pmapperCommands {
out = out + line
}
err = os.WriteFile(outFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("End of loot file."))
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), outFile)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), outFile)
}
@@ -243,53 +329,39 @@ func (m *IamSimulatorModule) executeChecks(wg *sync.WaitGroup, resource string,
func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, resource string, dataReceiver chan SimulatorResult) {
defer func() {
wg.Done()
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
for {
ListUsers, err := m.IAMListUsersClient.ListUsers(
context.TODO(),
&iam.ListUsersInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
for _, user := range ListUsers.Users {
//name := user.UserName
principal := user.Arn
adminCheckResult := m.isPrincipalAnAdmin(principal)
if adminCheckResult {
query := "Appears to be an administrator"
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: "",
}
} else {
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, user := range ListUsers {
//name := user.UserName
principal := user.Arn
adminCheckResult := m.isPrincipalAnAdmin(principal)
if adminCheckResult {
query := "Appears to be an administrator"
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: "",
}
} else {
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
// Pagination control. After the last page of output, the for loop exits.
if ListUsers.Marker != nil {
PaginationControl = ListUsers.Marker
} else {
PaginationControl = nil
break
}
}
}
@@ -297,106 +369,75 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, resource string, dataReceiver chan SimulatorResult) {
defer func() {
wg.Done()
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
for {
ListRoles, err := m.IAMListRolesClient.ListRoles(
context.TODO(),
&iam.ListRolesInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
for _, role := range ListRoles.Roles {
//name := user.UserName
principal := role.Arn
adminCheckResult := m.isPrincipalAnAdmin(principal)
if adminCheckResult {
query := "Appears to be an administrator"
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: "",
}
} else {
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, role := range ListRoles {
//name := user.UserName
principal := role.Arn
adminCheckResult := m.isPrincipalAnAdmin(principal)
if adminCheckResult {
query := "Appears to be an administrator"
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: "",
}
} else {
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
// Pagination control. After the last page of output, the for loop exits.
if ListRoles.Marker != nil {
PaginationControl = ListRoles.Marker
} else {
PaginationControl = nil
break
}
}
}
func (m *IamSimulatorModule) getPolicySimulatorResult(principal *string, actionNames []string, resource string, dataReceiver chan SimulatorResult) {
var PaginationControl2 *string
//var policySourceArn = "*"
//var arn *string
var resourceArns []string
resourceArns = append(resourceArns, resource)
//var resourceArns = []string{"*"}
for {
SimulatePrincipalPolicy, err := m.IAMSimulatePrincipalPolicyClient.SimulatePrincipalPolicy(
context.TODO(),
&iam.SimulatePrincipalPolicyInput{
Marker: PaginationControl2,
ActionNames: actionNames,
PolicySourceArn: principal,
ResourceArns: resourceArns,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
EvaluationResults, err := sdk.CachedIamSimulatePrincipalPolicy(m.IAMClient, aws.ToString(m.Caller.Account), principal, actionNames, resourceArns)
break
}
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
return
}
for _, result := range SimulatePrincipalPolicy.EvaluationResults {
evalName := result.EvalActionName
query := fmt.Sprintf("can %s on %s", *evalName, resource)
decision := result.EvalDecision
if decision == "allowed" {
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: string(decision),
}
for _, result := range EvaluationResults {
evalName := result.EvalActionName
query := fmt.Sprintf("can %s on %s", *evalName, resource)
decision := result.EvalDecision
if decision == "allowed" {
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: string(decision),
}
}
// Pagination control. After the last page of output, the for loop exits.
if SimulatePrincipalPolicy.Marker != nil {
PaginationControl2 = SimulatePrincipalPolicy.Marker
} else {
PaginationControl2 = nil
break
}
}
}
func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
@@ -409,10 +450,10 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"secretsmanager:GetSecretValue",
"ssm:GetDocument",
"ssm:GetParameters",
}
for {
SimulatePrincipalPolicy, err := m.IAMSimulatePrincipalPolicyClient.SimulatePrincipalPolicy(
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
context.TODO(),
&iam.SimulatePrincipalPolicyInput{
Marker: PaginationControl2,
@@ -422,11 +463,9 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
},
)
if err != nil {
//m.modLog.Error(err.Error())
TxtLogger.Println(err.Error())
m.CommandCounter.Error++
//m.modLog.Error(fmt.Sprintf("Failed admin check on %s\n\n", aws.ToString(principal)))
TxtLogger.Printf("Failed admin check on %s\n\n", aws.ToString(principal))
m.modLog.Error(err)
m.CommandCounter.IncrError()
m.modLog.Errorf("Failed admin check on %s", aws.ToString(principal))
return false
}
+256 -135
View File
@@ -10,9 +10,9 @@ import (
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
@@ -23,21 +23,24 @@ import (
type InstancesModule struct {
// General configuration data
EC2Client *ec2.Client
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
IAMListInstanceProfilesClient iam.ListInstanceProfilesAPIClient
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
UserDataAttributesOnly bool
AWSProfile string
WrapTable bool
InstanceProfileToRolesMap map[string][]iamTypes.Role
SkipAdminCheck bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
EC2Client sdk.AWSEC2ClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Goroutines int
UserDataAttributesOnly bool
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
InstanceProfileToRolesMap map[string][]iamTypes.Role
SkipAdminCheck bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Module's Results
MappedInstances []MappedInstance
@@ -63,8 +66,8 @@ type MappedInstance struct {
CanPrivEsc string
}
func (m *InstancesModule) Instances(filter string, outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *InstancesModule) Instances(filter string, outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "instances"
@@ -91,15 +94,15 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
fmt.Printf("[%s][%s] Enumerating EC2 instances in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
if m.pmapperError != nil {
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
} else {
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// } else {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
wg := new(sync.WaitGroup)
@@ -119,15 +122,19 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(instancesToSearch, region, wg, dataReceiver)
}
wg.Wait()
//time.Sleep(time.Second * 2)
// Perform role analysis
// Signal the receiver to stop and wait for it to finish draining the channel.
// This must happen BEFORE role analysis to ensure all MappedInstances are collected.
receiverDone <- true
<-receiverDone
// Perform role analysis (all instances are now in m.MappedInstances)
if m.pmapperError == nil {
for i := range m.MappedInstances {
m.MappedInstances[i].Admin, m.MappedInstances[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.MappedInstances[i].Role)
@@ -141,15 +148,13 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// This conditional block will either dump the userData attribute content or the general instances data, depending on what you select via command line.
//fmt.Printf("\n[*] Preparing output...\n\n")
if m.UserDataAttributesOnly {
m.printInstancesUserDataAttributesOnly(outputFormat, outputDirectory, dataReceiver)
m.printInstancesUserDataAttributesOnly(outputDirectory, dataReceiver)
} else {
m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver)
m.printGeneralInstanceData(outputDirectory, dataReceiver, verbosity)
}
}
@@ -167,17 +172,17 @@ func (m *InstancesModule) Receiver(receiver chan MappedInstance, receiverDone ch
}
}
func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputDirectory string, dataReceiver chan MappedInstance) {
defer func() {
m.output.CallingModule = "instances"
}()
m.output.CallingModule = "instance-userdata"
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)), "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
userDataFileName := filepath.Join(path, fmt.Sprintf("%s.txt", m.output.CallingModule))
@@ -203,13 +208,13 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
}
// only create a file if if there is at least one instance AND at least one instance had user-data.
if (len(m.MappedInstances) > 0) && (userDataOut != "=============================================\n") {
if m.output.Verbosity > 1 {
if m.output.Verbosity > 2 {
fmt.Printf("%s", userDataOut)
}
err = os.WriteFile(userDataFileName, []byte(userDataOut), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), userDataFileName)
} else {
@@ -217,10 +222,11 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
}
}
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
func (m *InstancesModule) printGeneralInstanceData(outputDirectory string, dataReceiver chan MappedInstance, verbosity int) {
// Prepare Table headers
//m.output.Headers = table.Row{
m.output.Headers = []string{
"Account",
//"ID",
"Name",
//"Arn",
@@ -233,12 +239,64 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
//"ID",
"Name",
//"Arn",
"ID",
"Zone",
"State",
"External IP",
"Internal IP",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
//"ID",
"Name",
//"Arn",
"ID",
"Zone",
"State",
"External IP",
"Internal IP",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
//Table rows
for _, instance := range m.MappedInstances {
m.output.Body = append(
m.output.Body,
//table.Row{
[]string{
aws.ToString(m.Caller.Account),
//instance.ID,
instance.Name,
//instance.Arn,
@@ -253,13 +311,28 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
////m.output.OutputSelector(outputFormat)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath)
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s instances found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -267,18 +340,23 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi
}
}
func (m *InstancesModule) writeLoot(outputDirectory string) {
func (m *InstancesModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
privateIPsFilename := filepath.Join(path, "instances-ec2PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "instances-ec2PublicIPs.txt")
ssmCommandsFilename := filepath.Join(path, "instances-ssmCommands.txt")
ec2InstanceConnectCommandsFilename := filepath.Join(path, "instances-ec2InstanceConnectCommands.txt")
var publicIPs string
var privateIPs string
var ssmCommands string
var ec2InstanceConnectCommands string
var headlineName string
for _, instance := range m.MappedInstances {
if instance.ExternalIP != "NoExternalIP" {
@@ -288,64 +366,116 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
privateIPs = privateIPs + fmt.Sprintln(instance.PrivateIP)
}
if instance.Name != "" {
headlineName = fmt.Sprintf("%s/%s", instance.Name, instance.ID)
} else {
headlineName = fmt.Sprintf("%s", instance.ID)
}
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ssmCommands = ssmCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ssmCommands = ssmCommands + fmt.Sprintf("### SSM start-session to %s ###\n", headlineName)
ssmCommands = ssmCommands + fmt.Sprintf("# You'll need the AWS CLI session manager plugin installed: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html\n")
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm start-session --target %s\n\n", instance.Region, instance.ID)
ssmCommands = ssmCommands + fmt.Sprintf("### SSM send-command to %s ###\n", headlineName)
ssmCommands = ssmCommands + fmt.Sprintf("# If you just want to run one command you can use send-command, but really, start-session is way easier\n")
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm send-command --instance-ids %s --document-name AWS-RunShellScript --parameters commands=\"aws sts get-caller-identity\" \n", instance.Region, instance.ID)
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm get-command-invocation --output text --instance-id %s --command-id <command-id-from-previous-command>\n\n", instance.Region, instance.ID)
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("### EC2 Instance Connect to %s ###\n", instance.ID)
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("# You'll need to change the --instance-os-user and --ssh-public-key parameters to match your own setup\n")
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("aws --profile $profile --region %s ec2-instance-connect send-ssh-public-key --instance-id %s --instance-os-user ec2-user --ssh-public-key file://~/.ssh/id_rsa.pub\n\n", instance.Region, instance.ID)
}
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(ssmCommandsFilename, []byte(ssmCommands), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
err = os.WriteFile(ec2InstanceConnectCommandsFilename, []byte(ec2InstanceConnectCommands), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ssmCommandsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ec2InstanceConnectCommandsFilename)
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Loot file for instance command:"))
fmt.Printf("Private IPs:\n\n")
fmt.Print(privateIPs)
fmt.Printf("Public IPs:\n\n")
fmt.Print(publicIPs)
fmt.Printf("SSM Commands:\n\n")
fmt.Print(ssmCommands)
fmt.Printf("EC2 Instance Connect Commands:\n\n")
fmt.Print(ec2InstanceConnectCommands)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
}
func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg *sync.WaitGroup, dataReceiver chan MappedInstance) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
m.modLog.Warn("ServiceMap is nil, creating new instance (this should not happen)")
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Error(err)
m.modLog.Errorf("Error checking if EC2 is available in region %s: %v", r, err)
m.CommandCounter.IncrError()
return
}
m.modLog.Debugf("ServiceMap check for EC2 in region %s: result=%v, error=%v", r, res, err)
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getDescribeInstances(instancesToSearch, r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
} else {
m.modLog.Infof("Skipping region %s: EC2 not available or check failed", r)
}
}
func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, region string) (userData *string, err error) {
Attributes, err := m.EC2Client.DescribeInstanceAttribute(
context.TODO(),
&ec2.DescribeInstanceAttributeInput{
InstanceId: instanceID,
Attribute: types.InstanceAttributeName("userData"),
},
func(o *ec2.Options) {
o.Region = region
},
)
UserData, err := sdk.CachedEC2DescribeInstanceAttributeUserData(m.EC2Client, aws.ToString(m.Caller.Account), region, aws.ToString(instanceID))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return nil, err
} else {
if Attributes.UserData.Value == nil {
if UserData == "" {
return aws.String("NoUserData"), nil
} else {
data, _ := base64.StdEncoding.DecodeString(*Attributes.UserData.Value)
data, _ := base64.StdEncoding.DecodeString(UserData)
return aws.String(string(data)), nil
}
}
@@ -355,53 +485,27 @@ func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, regio
func (m *InstancesModule) getDescribeInstances(instancesToSearch []string, region string, dataReceiver chan MappedInstance) {
// The "PaginationControl" value is nil when there's no more data to return.
var PaginationControl *string
for {
DescribeInstances, err := m.EC2Client.DescribeInstances(
context.TODO(),
&(ec2.DescribeInstancesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
Instances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, instance := range Instances {
if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
m.loadInstanceData(instance, region, dataReceiver)
}
for _, reservation := range DescribeInstances.Reservations {
accountId := reservation.OwnerId
for _, instance := range reservation.Instances {
if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
m.loadInstanceData(instance, region, accountId, dataReceiver)
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if DescribeInstances.NextToken != nil {
PaginationControl = DescribeInstances.NextToken
} else {
PaginationControl = nil
break
}
}
}
func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, accountId *string, dataReceiver chan MappedInstance) {
func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, dataReceiver chan MappedInstance) {
var profile string
var externalIP string
var name string = ""
var adminRole string = ""
var roleArn string = ""
var profileArn, profileName, name string
// The name is in a tag so we have to do this to grab the value from the right tag
for _, tag := range instance.Tags {
@@ -421,33 +525,50 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
if instance.IamInstanceProfile == nil {
profile = "NoInstanceProfile"
} else {
// This returns only the role name without the preceding forward slash.
profileARN := aws.ToString(instance.IamInstanceProfile.Arn)
profileID := aws.ToString(instance.IamInstanceProfile.Id)
profile = strings.Split(profileARN, "/")[len(strings.Split(profileARN, "/"))-1]
if roles, ok := m.InstanceProfileToRolesMap[profileID]; ok {
for _, role := range roles {
roleArn = aws.ToString(role.Arn)
}
dataReceiver <- MappedInstance{
ID: aws.ToString(instance.InstanceId),
Name: aws.ToString(&name),
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
State: string(instance.State.Name),
ExternalIP: externalIP,
PrivateIP: aws.ToString(instance.PrivateIpAddress),
Profile: profile,
Role: "",
Region: region,
Admin: adminRole,
CanPrivEsc: "",
}
}
dataReceiver <- MappedInstance{
ID: aws.ToString(instance.InstanceId),
Name: aws.ToString(&name),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
State: string(instance.State.Name),
ExternalIP: externalIP,
PrivateIP: aws.ToString(instance.PrivateIpAddress),
Profile: profile,
Role: roleArn,
Region: region,
Admin: adminRole,
CanPrivEsc: "",
}
} else {
profileArn = aws.ToString(instance.IamInstanceProfile.Arn)
// Extracting instance profile name from ARN
profileName = strings.Split(profileArn, "/")[len(strings.Split(profileArn, "/"))-1]
// Describe the IAM instance profile
profileOutput, err := sdk.CachedIamGetInstanceProfile(m.IAMClient, aws.ToString(m.Caller.Account), profileName)
if err != nil {
m.modLog.Errorf("failed to get instance profile for %s, %v", profileArn, err)
}
for _, role := range profileOutput.Roles {
dataReceiver <- MappedInstance{
ID: aws.ToString(instance.InstanceId),
Name: aws.ToString(&name),
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
State: string(instance.State.Name),
ExternalIP: externalIP,
PrivateIP: aws.ToString(instance.PrivateIpAddress),
Profile: profile,
Role: aws.ToString(role.Arn),
Region: region,
Admin: adminRole,
CanPrivEsc: "",
}
}
}
}
func (m *InstancesModule) getRolesFromInstanceProfiles() {
@@ -458,7 +579,7 @@ func (m *InstancesModule) getRolesFromInstanceProfiles() {
m.InstanceProfileToRolesMap = map[string][]iamTypes.Role{}
for PaginationControl {
ListInstanceProfiles, err := m.IAMListInstanceProfilesClient.ListInstanceProfiles(
ListInstanceProfiles, err := m.IAMClient.ListInstanceProfiles(
context.TODO(),
&(iam.ListInstanceProfilesInput{
Marker: PaginationMarker,
@@ -467,7 +588,7 @@ func (m *InstancesModule) getRolesFromInstanceProfiles() {
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, instanceProfile := range ListInstanceProfiles.InstanceProfiles {
+2427 -1065
View File
File diff suppressed because it is too large. Load diff
+136 -44
View File
@@ -7,11 +7,13 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/lambda"
"github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
@@ -21,19 +23,24 @@ import (
type LambdasModule struct {
// General configuration data
LambdaClient *lambda.Client
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
LambdaClient *lambda.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
iamSimClient IamSimulatorModule
// Main module data
Lambdas []Lambda
@@ -48,14 +55,16 @@ type Lambda struct {
Region string
Type string
Name string
Arn string
Role string
Admin string
CanPrivEsc string
Public string
}
func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "lambda"
@@ -68,15 +77,15 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
}
fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
if m.pmapperError != nil {
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
} else {
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// } else {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
@@ -96,7 +105,7 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -122,15 +131,52 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Service",
"Account",
"Region",
//"Type",
"Resource Arn",
"Name",
"Arn",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Arn",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
sort.Slice(m.Lambdas, func(i, j int) bool {
return m.Lambdas[i].AWSService < m.Lambdas[j].AWSService
})
@@ -141,10 +187,11 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
m.output.Body = append(
m.output.Body,
[]string{
m.Lambdas[i].AWSService,
aws.ToString(m.Caller.Account),
m.Lambdas[i].Region,
//m.Lambdas[i].Type,
m.Lambdas[i].Name,
m.Lambdas[i].Arn,
m.Lambdas[i].Role,
m.Lambdas[i].Admin,
m.Lambdas[i].CanPrivEsc,
@@ -154,10 +201,24 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath, verbosity)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s lambdas found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No lambdas found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -168,15 +229,19 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
func (m *LambdasModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("lambda", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getLambdasPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -200,7 +265,7 @@ func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "lambda-get-function-commands.txt")
@@ -219,12 +284,12 @@ func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
out = out + fmt.Sprintf("aws --profile $profile --region %s lambda get-function --function-name %s\n", function.Region, function.Name)
out = out + "# Download function code to to disk (requires jq and curl) \n"
out = out + fmt.Sprintf("mkdir -p ./lambdas/%s\n", function.Name)
out = out + fmt.Sprintf("url=`aws --profile $profile lambda get-function --region %s --function-name %s | jq .Code.Location | sed s/\"//g` && curl \"$url\" -o ./lambdas/%s.zip\n", function.Region, function.Name, function.Name)
out = out + fmt.Sprintf("url=`aws --profile $profile lambda get-function --region %s --function-name %s | jq .Code.Location | sed s/\\\"//g` && curl \"$url\" -o ./lambdas/%s.zip\n", function.Region, function.Name, function.Name)
}
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -241,8 +306,8 @@ func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -254,17 +319,18 @@ func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaph
functions, err := m.listFunctions(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, function := range functions {
//arn := aws.ToString(function.FunctionArn)
arn := aws.ToString(function.FunctionArn)
name := aws.ToString(function.FunctionName)
role := aws.ToString(function.Role)
dataReceiver <- Lambda{
AWSService: "Lambda",
Name: name,
Arn: arn,
Region: r,
Type: "",
Role: role,
@@ -293,8 +359,8 @@ func (m *LambdasModule) listFunctions(r string) ([]types.FunctionConfiguration,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
return functions, err
}
functions = append(functions, ListFunctions.Functions...)
@@ -308,3 +374,29 @@ func (m *LambdasModule) listFunctions(r string) ([]types.FunctionConfiguration,
}
return functions, nil
}
func (m *LambdasModule) getResourcePolicy(r string, functionName string) (policy.Policy, error) {
var projectPolicy policy.Policy
var policyJSON string
Policy, err := m.LambdaClient.GetPolicy(
context.TODO(),
&lambda.GetPolicyInput{
FunctionName: &functionName,
},
func(options *lambda.Options) {
options.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.IncrError()
return projectPolicy, err
}
policyJSON = aws.ToString(Policy.Policy)
projectPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
return projectPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", functionName, err)
}
return projectPolicy, nil
}
+2071
View File
File diff suppressed because it is too large. Load diff
+67
View File
@@ -0,0 +1,67 @@
package aws
import (
"fmt"
"sync"
"testing"
ec2_types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
// TestNetworkPortsCacheConcurrentAccess is a regression test for issue #129.
// Pre-fix, the cache lookup in getEC2NACLsPerRegion and getEC2SecurityGroupsPerRegion
// read m.nacls / m.securityGroups without holding a mutex while writes from
// other goroutines were locked, producing a "concurrent map read and map write"
// panic under load. Run with `go test -race ./aws/...` to verify the lock fix.
func TestNetworkPortsCacheConcurrentAccess(t *testing.T) {
m := &NetworkPortsModule{
nacls: make(map[string]*[]ec2_types.NetworkAcl),
securityGroups: make(map[string]*[]ec2_types.SecurityGroup),
}
// Pre-populate one region so reader goroutines hit the cached fast-path
// without needing a live EC2 client.
cachedNACLs := []ec2_types.NetworkAcl{}
m.nacls["cached"] = &cachedNACLs
cachedSGs := []ec2_types.SecurityGroup{}
m.securityGroups["cached"] = &cachedSGs
var wg sync.WaitGroup
const readers = 100
for i := 0; i < readers; i++ {
wg.Add(2)
go func() {
defer wg.Done()
_ = m.getEC2NACLsPerRegion("cached")
}()
go func() {
defer wg.Done()
_ = m.getEC2SecurityGroupsPerRegion("cached")
}()
}
// Writer goroutines populate uncached regions directly through the mutex,
// mirroring what the live functions do after a successful AWS fetch. The
// readers above must not race with these writes.
const writers = 10
for i := 0; i < writers; i++ {
region := fmt.Sprintf("uncached-%d", i)
wg.Add(2)
go func(r string) {
defer wg.Done()
sl := []ec2_types.NetworkAcl{}
m.naclsMutex.Lock()
m.nacls[r] = &sl
m.naclsMutex.Unlock()
}(region)
go func(r string) {
defer wg.Done()
sl := []ec2_types.SecurityGroup{}
m.securityGroupsMutex.Lock()
m.securityGroups[r] = &sl
m.securityGroupsMutex.Unlock()
}(region)
}
wg.Wait()
}
+325
View File
@@ -0,0 +1,325 @@
package aws
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/organizations/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/kyokomi/emoji"
"github.com/sirupsen/logrus"
)
type OrgModule struct {
OrganizationsClient sdk.OrganizationsClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
DescribeOrgOutput *types.Organization
// Main module data
Accounts []Account
Orgs map[string]Org
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Org struct {
OrgId string
MgmtAccount string
ChildAccounts []Account
}
type Account struct {
ProfileName string
isManagementAccount bool
Name string
Id string
Email string
Arn string
Status string
OrgId string
}
func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) {
// These struct values are used by the output module
var err error
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "org"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s][%s] Checking if account %s is the management account in an organization.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.DescribeOrgOutput, err = sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Errorf("Failed to describe organization: %s", err)
return
}
if err != nil {
m.modLog.Errorf("Failed to describe organization: %s", err)
fmt.Printf("[%s][%s] Account %s is either not associated with an organization, or you do not have the organizations:DescribeOrganization permission.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
} else {
if m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account)) {
m.addOrgAccounts()
} else {
m.addOrgAccount()
//fmt.Printf("[%s][%s] Account %s is not the management account in an organization %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
}
m.output.Headers = []string{
"Account",
"Name",
"ID",
"isManagementAccount?",
"Status",
"Email",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Name",
"ID",
"isManagementAccount?",
"Status",
"Email",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Name",
"ID",
"isManagementAccount?",
"Status",
"Email",
}
}
// Table rows
for i := range m.Accounts {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Accounts[i].Name,
m.Accounts[i].Id,
strconv.FormatBool(m.Accounts[i].isManagementAccount),
m.Accounts[i].Status,
m.Accounts[i].Email,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %d accounts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
}
}
func (m *OrgModule) ProcessMultipleAccounts(AWSProfiles []string, version string) {
var seenOrgs []string
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan Account)
// Create a channel to signal to stop
receiverDone := make(chan bool)
// Create a channel to signal to stop
go m.Receiver(dataReceiver, receiverDone)
for _, profile := range AWSProfiles {
wg.Add(1)
m.CommandCounter.IncrPending()
go m.FindMgmtAccounts(profile, version, wg, semaphore, dataReceiver)
wg.Wait()
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
orgId := aws.ToString(m.DescribeOrgOutput.Id)
// if this orgId is not in seenOrgs, add it to seenOrgs
if !internal.Contains(orgId, seenOrgs) {
seenOrgs = append(seenOrgs, orgId)
// add this org to the Orgs map
Org := Org{
OrgId: orgId,
}
m.Orgs[orgId] = Org
} else {
continue
}
isMgmtAccount := m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account))
if isMgmtAccount {
mgmtAccount := aws.ToString(m.Caller.Account)
fmt.Printf("[%s] Found an Organization Management Account: %s\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), mgmtAccount)
//mgmtAccounts[mgmtAccount] = append(mgmtAccounts[mgmtAccount], profile)
}
}
}
func (m *OrgModule) Receiver(receiver chan Account, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Accounts = append(m.Accounts, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *OrgModule) FindMgmtAccounts(profile string, version string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Account) {
var err error
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Errorf("Failed to describe organization: %s", err)
m.CommandCounter.IncrError()
return
}
dataReceiver <- Account{
isManagementAccount: m.IsManagementAccount(DescribeOrganization, aws.ToString(m.Caller.Account)),
Name: aws.ToString(m.Caller.Account),
Id: aws.ToString(m.Caller.Account),
Status: "ACTIVE",
Email: aws.ToString(m.Caller.Account),
OrgId: aws.ToString(DescribeOrganization.Id),
Arn: aws.ToString(m.Caller.Account),
}
}
func (m *OrgModule) IsManagementAccount(Organization *types.Organization, account string) bool {
// Check if the account is the management account
// https://docs.aws.amazon.com/organizations/latest/APIReference/API_DescribeOrganization.html
if aws.ToString(Organization.MasterAccountId) == account {
return true
}
return false
}
func (m *OrgModule) addOrgAccounts() {
accounts, err := sdk.CachedOrganizationsListAccounts(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
for _, account := range accounts {
m.Accounts = append(m.Accounts, Account{
isManagementAccount: m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(account.Id)),
Name: aws.ToString(account.Name),
Id: aws.ToString(account.Id),
Email: aws.ToString(account.Email),
Arn: aws.ToString(account.Arn),
Status: string(account.Status),
})
}
}
func (m *OrgModule) addOrgAccount() {
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Errorf("Failed to describe organization: %s", err)
m.CommandCounter.IncrError()
return
}
m.Accounts = append(m.Accounts, Account{
isManagementAccount: true,
Name: "Mgmt Account",
Id: aws.ToString(DescribeOrganization.MasterAccountId),
Email: aws.ToString(DescribeOrganization.MasterAccountEmail),
Arn: aws.ToString(DescribeOrganization.MasterAccountArn),
Status: "ACTIVE",
})
m.Accounts = append(m.Accounts, Account{
isManagementAccount: false,
Name: "This account",
Id: aws.ToString(m.Caller.Account),
Email: "Unknown",
Arn: aws.ToString(m.Caller.Arn),
Status: "ACTIVE",
})
}
func (m *OrgModule) IsCallerAccountPartOfAnOrg() bool {
DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Errorf("Failed to describe organization: %s", err)
return false
} else {
m.DescribeOrgOutput = DescribeOrgOutput
}
return true
}
+211 -22
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -22,12 +23,15 @@ type OutboundAssumedRolesModule struct {
// General configuration data
CloudTrailClient *cloudtrail.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
OutboundAssumeRoleEntries []OutboundAssumeRoleEntry
@@ -47,6 +51,7 @@ type OutboundAssumeRoleEntry struct {
SourcePrincipal string
DestinationAccount string
DestinationPrincipal string
Action string
LogTimestamp string
}
@@ -114,8 +119,26 @@ type CloudTrailEvent struct {
} `json:"tlsDetails"`
}
func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
var interestingCrossAccountEventNames = []string{
"AssumeRole",
"AssumeRoleWithSAML",
"AssumeRoleWithWebIdentity",
"GetObject",
"ListBuckets",
"BatchGetImage",
"GetDownloadUrlForLayer",
"SendMessage",
"GetQueueUrl",
"Invoke20150331",
"RunInstances",
"RunTask",
"StartTask",
"CreateTask",
"CreateTaskSet",
}
func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "outbound-assumed-roles"
@@ -148,7 +171,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -161,28 +184,68 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
<-receiverDone
m.output.Headers = []string{
"Service",
"Account",
"Region",
"Type",
//"Source Account",
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Type",
//"Source Account",
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Region",
"Type",
//"Source Account",
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
}
// Table rows
for i := range m.OutboundAssumeRoleEntries {
m.output.Body = append(
m.output.Body,
[]string{
m.OutboundAssumeRoleEntries[i].AWSService,
aws.ToString(m.Caller.Account),
m.OutboundAssumeRoleEntries[i].Region,
m.OutboundAssumeRoleEntries[i].Type,
//m.OutboundAssumeRoleEntries[i].SourceAccount,
m.OutboundAssumeRoleEntries[i].SourcePrincipal,
//m.OutboundAssumeRoleEntries[i].DestinationAccount,
m.OutboundAssumeRoleEntries[i].DestinationPrincipal,
m.OutboundAssumeRoleEntries[i].Action,
m.OutboundAssumeRoleEntries[i].LogTimestamp,
},
)
@@ -190,10 +253,24 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s log entries found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
//m.writeLoot()
@@ -218,25 +295,32 @@ func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEn
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("cloudtrail", r)
if err != nil {
m.modLog.Error(err)
}
if res {
// wg.Add(1)
// m.CommandCounter.IncrTotal()
// m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
wg.Add(1)
m.CommandCounter.Total++
m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.IncrTotal()
m.getCrossAccountBatchGetImageEntriesPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -274,7 +358,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -306,6 +390,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
SourcePrincipal: sourcePrincipal,
DestinationAccount: destinationAccount,
DestinationPrincipal: destinationPrincipal,
Action: "sts:AssumeRole",
LogTimestamp: logTimestamp,
}
}
@@ -323,3 +408,107 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
}
}
// get cross account batch get image entries
func (m *OutboundAssumedRolesModule) getCrossAccountBatchGetImageEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer func() {
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
//var LookupAttributes []types.LookupAttributes
//var LookupAttribute types.LookupAttribute
var pages int
days := 0 - m.Days
endTime := aws.Time(time.Now())
startTime := endTime.AddDate(0, 0, days)
for {
LookupEvents, err := m.CloudTrailClient.LookupEvents(
context.TODO(),
&cloudtrail.LookupEventsInput{
EndTime: endTime,
StartTime: &startTime,
// LookupAttributes: []cloudtrailTypes.LookupAttribute{
// {
// AttributeKey: cloudtrailTypes.LookupAttributeKeyEventName,
// AttributeValue: aws.String("BatchGetImage"),
// },
// },
NextToken: PaginationControl,
},
func(o *cloudtrail.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
break
}
for _, event := range LookupEvents.Events {
//eventData := *event.CloudTrailEvent
//fmt.Println(eventData)
var sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal, userType string
cloudtrailEvent := CloudTrailEvent{}
json.Unmarshal([]byte(*event.CloudTrailEvent), &cloudtrailEvent)
for _, eventName := range interestingCrossAccountEventNames {
if aws.ToString(event.EventName) == eventName {
// extract the source account and principal
if cloudtrailEvent.UserIdentity.Type == "AssumedRole" || cloudtrailEvent.UserIdentity.Type == "IAMUser" || cloudtrailEvent.UserIdentity.Type == "Role" {
if cloudtrailEvent.UserIdentity.Type == "AssumedRole" {
sourcePrincipal = cloudtrailEvent.UserIdentity.SessionContext.SessionIssuer.Arn
} else {
sourcePrincipal = cloudtrailEvent.UserIdentity.Arn
}
userType = cloudtrailEvent.UserIdentity.Type
sourceAccount = cloudtrailEvent.UserIdentity.AccountID
//fmt.Printf("%s,%s,%s,%s\n", sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal)
if cloudtrailEvent.Resources != nil {
destinationAccount = cloudtrailEvent.Resources[0].AccountID
destinationPrincipal = cloudtrailEvent.Resources[0].Arn
if sourceAccount != destinationAccount {
logTimestamp := cloudtrailEvent.EventTime.Format("2006-01-02 15:04:05")
dataReceiver <- OutboundAssumeRoleEntry{
AWSService: "CloudTrail",
Region: r,
Type: userType,
SourceAccount: sourceAccount,
SourcePrincipal: sourcePrincipal,
DestinationAccount: destinationAccount,
DestinationPrincipal: destinationPrincipal,
Action: aws.ToString(event.EventName),
LogTimestamp: logTimestamp,
}
}
}
}
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if LookupEvents.NextToken != nil {
PaginationControl = LookupEvents.NextToken
pages++
} else {
PaginationControl = nil
break
}
}
}
+384 -246
View File
@@ -1,18 +1,18 @@
package aws
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/BishopFox/cloudfox/internal/common"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
@@ -20,14 +20,16 @@ import (
type IamPermissionsModule struct {
// General configuration data
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
@@ -35,7 +37,7 @@ type IamPermissionsModule struct {
Users []GAADUser
Roles []GAADRole
Groups []GAADGroup
Rows []PermissionsRow
Rows []common.PermissionsRow
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
@@ -70,50 +72,40 @@ type GAADGroup struct {
InlinePolicies []types.PolicyDetail
}
type PermissionsRow struct {
AWSService string
Type string
Name string
Arn string
PolicyType string
PolicyName string
Effect string
Action string
Resource string
}
func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDirectory string, verbosity int, principal string) {
// These stuct values are used by the output module
func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbosity int, principal string) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "permissions"
m.output.FullFilename = m.output.CallingModule
filename := m.output.CallingModule
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s][%s] Enumerating IAM permissions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
if principal != "" {
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
}
m.getGAAD(principal)
m.parsePermissions()
m.GetGAAD()
m.ParsePermissions(principal)
m.output.Headers = []string{
"Service",
"Principal Type",
"Account",
"Type",
"Name",
//"Arn",
"Policy Type",
"Arn",
"Policy",
"Policy Name",
"Policy Arn",
"Effect",
"Action",
"Resource",
"Condition",
}
//Table rows
@@ -121,25 +113,78 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
m.output.Body = append(
m.output.Body,
[]string{
m.Rows[i].AWSService,
aws.ToString(m.Caller.Account),
m.Rows[i].Type,
m.Rows[i].Name,
//m.Rows[i].Arn,
m.Rows[i].Arn,
m.Rows[i].PolicyType,
m.Rows[i].PolicyName,
m.Rows[i].PolicyArn,
m.Rows[i].Effect,
m.Rows[i].Action,
m.Rows[i].Resource,
m.Rows[i].Condition,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector3(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Type",
//"Name",
"Arn",
"Policy",
"Policy Name",
"Policy Arn",
"Effect",
"Action",
"Resource",
"Condition"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{
"Type",
"Name",
//"Arn",
"Policy",
"Policy Name",
"Effect",
"Action",
"Resource",
"Condition",
}
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
TableCols: tableCols,
Body: m.output.Body,
Name: filename,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s unique permissions identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -148,192 +193,283 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *IamPermissionsModule) getGAAD(principal string) {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
//var totalRoles int
// var attachedPolicies []types.AttachedPolicy
// var inlinePolicies []types.PolicyDetail
for {
GAAD, err := m.IAMClient.GetAccountAuthorizationDetails(
context.TODO(),
&iam.GetAccountAuthorizationDetailsInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, policy := range GAAD.Policies {
//var IAMtype = "Role"
arn := aws.ToString(policy.Arn)
name := aws.ToString(policy.PolicyName)
m.Policies = append(m.Policies, GAADPolicy{
Arn: arn,
Name: name,
PolicyVersionList: policy.PolicyVersionList,
})
}
for _, role := range GAAD.RoleDetailList {
arn := aws.ToString(role.Arn)
name := aws.ToString(role.RoleName)
if principal == "" {
m.Roles = append(m.Roles, GAADRole{
Arn: arn,
Name: name,
AttachedPolicies: role.AttachedManagedPolicies,
InlinePolicies: role.RolePolicyList,
})
} else {
if arn == principal {
m.Roles = append(m.Roles, GAADRole{
Arn: arn,
Name: name,
AttachedPolicies: role.AttachedManagedPolicies,
InlinePolicies: role.RolePolicyList,
})
}
}
}
// i think the error here is pagination!!
for _, user := range GAAD.UserDetailList {
//var IAMtype = "User"
arn := aws.ToString(user.Arn)
name := aws.ToString(user.UserName)
groupList := user.GroupList
if principal == "" {
m.Users = append(m.Users, GAADUser{
Arn: arn,
Name: name,
AttachedPolicies: user.AttachedManagedPolicies,
InlinePolicies: user.UserPolicyList,
GroupList: groupList,
})
} else {
if arn == principal {
m.Users = append(m.Users, GAADUser{
Arn: arn,
Name: name,
AttachedPolicies: user.AttachedManagedPolicies,
InlinePolicies: user.UserPolicyList,
GroupList: groupList,
})
}
}
}
for _, group := range GAAD.GroupDetailList {
arn := aws.ToString(group.Arn)
name := aws.ToString(group.GroupName)
if principal == "" {
m.Groups = append(m.Groups, GAADGroup{
Arn: arn,
Name: name,
AttachedPolicies: group.AttachedManagedPolicies,
InlinePolicies: group.GroupPolicyList,
})
} else {
if arn == principal {
m.Groups = append(m.Groups, GAADGroup{
Arn: arn,
Name: name,
AttachedPolicies: group.AttachedManagedPolicies,
InlinePolicies: group.GroupPolicyList,
})
}
}
}
// Pagination control. After the last page of output, the for loop exits.
if GAAD.Marker != nil {
PaginationControl = GAAD.Marker
} else {
PaginationControl = nil
break
}
func (m *IamPermissionsModule) GetGAAD() {
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err)
m.CommandCounter.IncrError()
return
}
// // if user supplied a principal name without the arn, try to create the arn
// if !strings.Contains(principal, "arn:") {
// principal = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
// }
for _, policy := range GAAD.Policies {
//var IAMtype = "Role"
arn := aws.ToString(policy.Arn)
name := aws.ToString(policy.PolicyName)
m.Policies = append(m.Policies, GAADPolicy{
Arn: arn,
Name: name,
PolicyVersionList: policy.PolicyVersionList,
})
}
for _, role := range GAAD.RoleDetailList {
arn := aws.ToString(role.Arn)
name := aws.ToString(role.RoleName)
m.Roles = append(m.Roles, GAADRole{
Arn: arn,
Name: name,
AttachedPolicies: role.AttachedManagedPolicies,
InlinePolicies: role.RolePolicyList,
})
}
for _, user := range GAAD.UserDetailList {
//var IAMtype = "User"
arn := aws.ToString(user.Arn)
name := aws.ToString(user.UserName)
groupList := user.GroupList
m.Users = append(m.Users, GAADUser{
Arn: arn,
Name: name,
AttachedPolicies: user.AttachedManagedPolicies,
InlinePolicies: user.UserPolicyList,
GroupList: groupList,
})
}
for _, group := range GAAD.GroupDetailList {
arn := aws.ToString(group.Arn)
name := aws.ToString(group.GroupName)
m.Groups = append(m.Groups, GAADGroup{
Arn: arn,
Name: name,
AttachedPolicies: group.AttachedManagedPolicies,
InlinePolicies: group.GroupPolicyList,
})
}
}
func (m *IamPermissionsModule) parsePermissions() {
// create a function that will take a principal name and try to see if it is a role, user, or group and return the arn of the principal
func (m *IamPermissionsModule) getPrincipalArn(principal string) string {
var arn string
for _, role := range m.Roles {
if role.Name == principal {
arn = role.Arn
}
}
for _, user := range m.Users {
if user.Name == principal {
arn = user.Arn
}
}
for _, group := range m.Groups {
if group.Name == principal {
arn = group.Arn
}
}
return arn
}
func (m *IamPermissionsModule) ParsePermissions(principal string) {
var inputArn string
for i := range m.Roles {
if principal == "" {
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
}
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
}
} else {
// if user supplied a principal name without the arn, try to create the arn
if !strings.Contains(principal, "arn:") {
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
} else {
inputArn = principal
}
if strings.ToLower(m.Roles[i].Arn) == strings.ToLower(inputArn) {
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
}
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
}
}
}
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
}
}
for i := range m.Users {
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
if principal == "" {
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range m.Users[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
} else {
// if user supplied a principal name without the arn, try to create the arn
if !strings.Contains(principal, "arn:") {
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
} else {
inputArn = principal
}
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range m.Users[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
}
}
for _, inlinePolicy := range m.Users[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
for j, group := range m.Users[i].GroupList {
for _, gaadGroup := range m.Groups {
if gaadGroup.Name == group {
for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
// for each group in the user's group list, get the attached and inline policy names, and then get the permissions from those policies
for g := range m.Users[i].GroupList {
if principal == "" {
for _, group := range m.Groups {
if group.Name == m.Users[i].GroupList[g] {
for _, attachedPolicy := range group.AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range group.InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
}
for _, inlinePolicy := range m.Groups[j].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
} else {
// if user supplied a principal name without the arn, try to create the arn
if !strings.Contains(principal, "arn:") {
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
} else {
inputArn = principal
}
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
for _, group := range m.Groups {
if group.Name == m.Users[i].GroupList[g] {
for _, attachedPolicy := range group.AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range group.InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
}
}
}
}
}
// for group := range m.Users[i].GroupList {
// for _, gaadGroup := range m.Groups {
// if gaadGroup.Name == group {
// for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
// m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
// }
// for _, inlinePolicy := range m.Groups[j].InlinePolicies {
// m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
// }
// }
// }
// }
}
}
func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, attachedPolicy types.AttachedPolicy, IAMtype string, name string) {
//var policies []types.ManagedPolicyDetail
var s StatementEntry
var s policy.PolicyStatement
var AWSService = "IAM"
var hasConditions string
for _, p := range m.Policies {
if p.Name == aws.ToString(attachedPolicy.PolicyName) {
for _, d := range p.PolicyVersionList {
if d.IsDefaultVersion {
parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
//parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
document, _ := url.QueryUnescape(aws.ToString(d.Document))
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
// hasStsAssumeRole := parsedPolicyDocument.DoesPolicyHaveMatchingStatement("Allow", "sts:AssumeRole", "*")
// fmt.Println(hasStsAssumeRole)
for _, s = range parsedPolicyDocument.Statement {
//version := parsedPolicyDocument.Version
effect := s.Effect
for _, action := range s.Action {
for _, resource := range s.Resource {
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
Effect: effect,
Action: action,
Resource: resource,
})
if s.Action != nil {
for _, action := range s.Action {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
PolicyArn: p.Arn,
Effect: effect,
Action: action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
if s.NotAction != nil {
for _, action := range s.NotAction {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
PolicyArn: p.Arn,
Effect: effect,
Action: "[NotAction] " + action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
}
}
}
@@ -343,66 +479,68 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inlinePolicy types.PolicyDetail, IAMtype string, name string) {
//var policies []types.ManagedPolicyDetail
var s StatementEntry
var s policy.PolicyStatement
var AWSService = "IAM"
var hasConditions string
//parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
document, _ := url.QueryUnescape(aws.ToString(inlinePolicy.PolicyDocument))
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
for _, s = range parsedPolicyDocument.Statement {
//version := parsedPolicyDocument.Version
effect := s.Effect
for _, action := range s.Action {
for _, resource := range s.Resource {
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: action,
Resource: resource,
})
if s.Action != nil {
for _, action := range s.Action {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
PolicyArn: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
if s.NotAction != nil {
for _, action := range s.NotAction {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
PolicyArn: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: "[NotAction] " + action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
}
}
type policyDocument struct {
Version string `json:"Version"`
Statement []StatementEntry `json:"Statement"`
}
type StatementEntry struct {
Effect string `json:"Effect"`
Action ListOfItems `json:"Action"`
Resource ListOfItems `json:"Resource"`
Condition ListOfItems `json:"Condition"`
}
func parsePolicyDocument(doc *string) (policyDocument, error) {
document, _ := url.QueryUnescape(aws.ToString(doc))
var parsedDocumentToJSON policyDocument
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
return parsedDocumentToJSON, nil
}
// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
type ListOfItems []string
func (r *ListOfItems) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err == nil {
*r = append(*r, s)
return nil
}
var ss []string
if err := json.Unmarshal(b, &ss); err == nil {
*r = ss
return nil
}
return errors.New("cannot unmarshal neither to a string nor a slice of strings")
}
+441 -45
View File
@@ -1,39 +1,52 @@
package aws
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/dominikbraun/graph"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/sirupsen/logrus"
)
type PmapperModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
pmapperGraph graph.Graph[string, string]
Nodes []Node
Edges []Edge
CommandCounter internal.CommandCounter
PmapperDataBasePath string
pmapperGraph graph.Graph[string, string]
Nodes []Node
Edges []Edge
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type PmapperOutputRow struct {
Start string
End string
Paths []string
}
type Edge struct {
Source string `json:"source"`
Destination string `json:"destination"`
@@ -42,19 +55,54 @@ type Edge struct {
}
type Node struct {
Arn string `json:"arn"`
IDValue string `json:"id_value"`
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
GroupMemberships []interface{} `json:"group_memberships"`
TrustPolicy interface{} `json:"trust_policy"`
InstanceProfile interface{} `json:"instance_profile"`
ActivePassword bool `json:"active_password"`
AccessKeys int `json:"access_keys"`
IsAdmin bool `json:"is_admin"`
PermissionsBoundary interface{} `json:"permissions_boundary"`
HasMfa bool `json:"has_mfa"`
Tags Tags `json:"tags"`
PathToAdmin bool
Arn string `json:"arn"`
Type string
AccountID string
Name string
IDValue string `json:"id_value"`
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
GroupMemberships []interface{} `json:"group_memberships"`
TrustPolicy interface{} `json:"trust_policy"`
TrustsDoc policy.TrustPolicyDocument
TrustedPrincipals []TrustedPrincipal
TrustedServices []TrustedService
TrustedFederatedProviders []TrustedFederatedProvider
InstanceProfile interface{} `json:"instance_profile"`
ActivePassword bool `json:"active_password"`
AccessKeys int `json:"access_keys"`
IsAdmin bool `json:"is_admin"`
PathToAdmin bool
PermissionsBoundary interface{} `json:"permissions_boundary"`
HasMfa bool `json:"has_mfa"`
Tags Tags `json:"tags"`
CanPrivEscToAdminString string
IsAdminString string
VendorName string
}
type TrustedPrincipal struct {
TrustedPrincipal string
ExternalID string
VendorName string
AccountIsInAnalyzedAccountList bool
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedService struct {
TrustedService string
AccountID string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedFederatedProvider struct {
TrustedFederatedProvider string
ProviderAccountId string
ProviderShortName string
TrustedSubjects []string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type AttachedPolicies struct {
@@ -71,35 +119,95 @@ func (m *PmapperModule) initPmapperGraph() error {
return err
}
m.pmapperGraph = graph.New(graph.StringHash, graph.Directed())
for _, node := range m.Nodes {
_ = m.pmapperGraph.AddVertex(node.Arn)
}
for _, edge := range m.Edges {
_ = m.pmapperGraph.AddEdge(edge.Source, edge.Destination)
}
m.pmapperGraph = m.createAndPopulateGraph()
for i := range m.Nodes {
if m.doesNodeHavePathToAdmin(m.Nodes[i]) {
m.Nodes[i].PathToAdmin = true
m.Nodes[i].CanPrivEscToAdminString = "Yes"
//fmt.Println(m.Nodes[i].Arn, m.Nodes[i].IsAdmin, m.Nodes[i].PathToAdmin)
} else {
m.Nodes[i].PathToAdmin = false
m.Nodes[i].CanPrivEscToAdminString = "No"
}
if m.Nodes[i].IsAdmin {
m.Nodes[i].IsAdminString = "Yes"
} else {
m.Nodes[i].IsAdminString = "No"
}
}
return nil
}
func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
pmapperGraph := graph.New(graph.StringHash, graph.Directed())
// having issues with caching the graph. will have to swing back and try again later
// gob.Register(pmapperGraph)
// cacheKey := fmt.Sprintf("%s-pmapperGraph", aws.ToString(m.Caller.Account))
// cached, found := internal.Cache.Get(cacheKey)
// if found {
// return cached.(graph.Graph[string, string])
// }
for _, node := range m.Nodes {
_ = pmapperGraph.AddVertex(node.Arn)
}
for _, edge := range m.Edges {
err := pmapperGraph.AddEdge(
edge.Source,
edge.Destination,
graph.EdgeAttribute(edge.ShortReason, edge.Reason),
)
if err != nil {
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
//fmt.Println("Edge already exists, but adding a new one!")
// get the existing edge
existingEdge, _ := pmapperGraph.Edge(edge.Source, edge.Destination)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes[edge.ShortReason] = edge.Reason
//Update the edge
pmapperGraph.UpdateEdge(
edge.Source,
edge.Destination,
graph.EdgeAttributes(existingProperties.Attributes),
)
}
//fmt.Println(edge.Reason)
}
}
return nil
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
return pmapperGraph
}
func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool {
for i := range m.Nodes {
if m.Nodes[i].Arn == principal {
if m.Nodes[i].PathToAdmin {
m.Nodes[i].CanPrivEscToAdminString = "Yes"
return true
} else {
m.Nodes[i].CanPrivEscToAdminString = "No"
return false
}
}
@@ -111,7 +219,11 @@ func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
for i := range m.Nodes {
if m.Nodes[i].Arn == principal {
if m.Nodes[i].IsAdmin {
m.Nodes[i].IsAdminString = "Yes"
return true
} else {
m.Nodes[i].IsAdminString = "No"
return false
}
}
@@ -119,8 +231,8 @@ func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
return false
}
func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "pmapper"
@@ -131,11 +243,12 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
pmapperError := m.initPmapperGraph()
if pmapperError != nil {
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, this cloudfox command and others will attempt to find and use the pmapper graph data in this and other cloudfox commands\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, cloudfox will attempt to enrich this command and others with pmapper privesc data\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
fmt.Printf("[%s][%s] For more info and troubleshooting steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
m.modLog.Error(pmapperError)
return
@@ -145,11 +258,39 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st
}
m.output.Headers = []string{
"Account",
"Principal Arn",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Principal Arn",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Principal Arn",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
}
//Table rows
var isAdmin, pathToAdmin string
for i := range m.Nodes {
@@ -168,6 +309,7 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Nodes[i].Arn,
isAdmin,
pathToAdmin,
@@ -177,9 +319,41 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
Loot: internal.LootClient{
DirectoryName: m.output.FilePath,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
header, body := m.createPmapperTableData(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: header,
Body: body,
Name: "pmapper-privesc-paths-enhanced",
SkipPrintToScreen: true,
})
loot := m.writeLoot(o.Table.DirectoryName, verbosity)
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
Name: m.output.CallingModule,
Contents: loot,
})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
@@ -199,9 +373,6 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
for _, p := range path {
if p != "" {
if startNode.Arn != destNode.Arn {
// if we got here there is a path
//fmt.Printf("%s has a path %s who is an admin.\n", startNode.Arn, destNode.Arn)
//fmt.Println(path)
return true
}
@@ -213,9 +384,136 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
return false
}
func (m *PmapperModule) createPmapperTableData(outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
header = []string{
"Start",
"End",
"Path(s)",
}
var paths string
var admins, privescPathsBody [][]string
for _, startNode := range m.Nodes {
if startNode.IsAdmin {
admins = append(admins, []string{startNode.Arn, "", "ADMIN"})
} else {
for _, destNode := range m.Nodes {
if destNode.IsAdmin {
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
// if we have a path,
if len(path) > 0 {
if startNode.Arn != destNode.Arn {
paths = ""
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
for i := 0; i < len(path)-1; i++ {
for _, edge := range m.Edges {
if edge.Source == path[i] && edge.Destination == path[i+1] {
//Some pmapper reasons have commas in them so lets get rid of them in the csvOutputdata
edge.Reason = strings.ReplaceAll(edge.Reason, ",", " and")
paths += fmt.Sprintf("%s %s %s\n", path[i], edge.Reason, path[i+1])
}
}
}
//trim the last newline from csvPaths
paths = strings.TrimSuffix(paths, "\n")
privescPathsBody = append(privescPathsBody, []string{startNode.Arn, destNode.Arn, paths})
}
}
}
}
}
}
// create body by first adding the admins and then the privesc paths
body = append(body, admins...)
body = append(body, privescPathsBody...)
return header, body
}
func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
panic(err.Error())
}
lootFilePath := filepath.Join(path, "pmapper.txt")
var admins, out string
for _, startNode := range m.Nodes {
if startNode.IsAdmin {
admins += fmt.Sprintf("ADMIN FOUND: %s\n", startNode.Arn)
} else {
for _, destNode := range m.Nodes {
if destNode.IsAdmin {
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
// if we have a path,
if len(path) > 0 {
if startNode.Arn != destNode.Arn {
// if we got here there is a path
out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn)
//fmt.Println(path)
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
for i := 0; i < len(path)-1; i++ {
for _, edge := range m.Edges {
if edge.Source == path[i] && edge.Destination == path[i+1] {
// print it like this: [start node] [reason] [end node]
out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
}
// shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice
//for _, edge := range GlobalPmapperEdges {
// if edge.Source == path[i] && edge.Destination == path[i+1] {
// // print it like this: [start node] [reason] [end node]
// out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
// }
// }
}
}
out += fmt.Sprintf("\n")
}
}
}
}
}
}
out = admins + "\n\n" + out
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file"))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file"))
}
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), magenta(fmt.Sprintf("Loot file with ALL potential paths written to: [%s]", lootFilePath)))
return out
}
func (m *PmapperModule) readPmapperData(accountID *string) error {
e, n := generatePmapperDataBasePaths(accountID)
var e, n string
if m.PmapperDataBasePath == "" {
e, n = generatePmapperDataBasePaths(accountID)
} else {
e = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "edges.json")
n = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "nodes.json")
}
nodesFile, err := os.Open(n)
if err != nil {
@@ -236,3 +534,101 @@ func (m *PmapperModule) readPmapperData(accountID *string) error {
return nil
}
func (m *PmapperModule) GenerateCypherStatements(goCtx context.Context, driver neo4j.DriverWithContext) error {
// Insert nodes
for i, node := range m.Nodes {
query, params := m.generateNodeCreateStatement(node, i)
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
return err
}
}
// Insert edges
for i, edge := range m.Edges {
query, params := m.generateEdgeCreateStatement(edge, i)
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
return err
}
}
return nil
}
func (m *PmapperModule) generateNodeCreateStatement(node Node, i int) (string, map[string]interface{}) {
var ptype, label, query string
var params map[string]any
if strings.Contains(node.Arn, "role") {
label = GetResourceNameFromArn(node.Arn)
ptype = "Role"
params = map[string]any{
"Id": node.Arn,
"ARN": node.Arn,
"Name": GetResourceNameFromArn(node.Arn),
"IdValue": node.IDValue,
"IsAdminP": node.IsAdmin,
"PathToAdmin": node.PathToAdmin,
}
} else if strings.Contains(node.Arn, "user") {
label = GetResourceNameFromArn(node.Arn)
ptype = "User"
//node.TrustPolicy = ""
params = map[string]any{
"Id": node.Arn,
"ARN": node.Arn,
"Name": GetResourceNameFromArn(node.Arn),
"IdValue": node.IDValue,
"IsAdminP": node.IsAdmin,
"PathToAdmin": node.PathToAdmin,
}
} else if strings.Contains(node.Arn, "group") {
label = GetResourceNameFromArn(node.Arn)
ptype = "Group"
}
label = strings.ReplaceAll(label, "-", "_")
label = strings.ReplaceAll(label, ".", "_")
query = `MERGE (%s:%s {Id: $Id, ARN: $ARN, Name: $Name, IdValue: $IdValue, IsAdminP: $IsAdminP, PathToAdmin: $PathToAdmin})`
//sanitizedArn := sanitizeArnForNeo4jLabel(node.Arn)
//id := fmt.Sprintf("%s_%s", sanitizedArn, ptype)
fmt.Println(fmt.Sprintf(query, label, ptype), params)
return fmt.Sprintf(query, label, ptype), params
}
func (m *PmapperModule) generateEdgeCreateStatement(edge Edge, i int) (string, map[string]interface{}) {
// Sanitize ARNs for matching nodes
//srcArnSanitized := sanitizeArnForNeo4jLabel(edge.Source)
//destArnSanitized := sanitizeArnForNeo4jLabel(edge.Destination)
query := `MATCH (a {ARN: $srcArn}), (b {ARN: $destArn}) CREATE (a)-[:CAN_ACCESS {reason: $reason, shortReason: $shortReason}]->(b)`
params := map[string]any{
"srcArn": edge.Source,
"destArn": edge.Destination,
"reason": edge.Reason,
"shortReason": edge.ShortReason,
}
fmt.Println(query, params)
return query, params
}
func (m *PmapperModule) executeCypherQuery(ctx context.Context, driver neo4j.DriverWithContext, query string, params map[string]interface{}) error {
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
sharedLogger.Errorf("Error executing query: %s -- %v", err, params)
return err
}
return nil
}
func sanitizeArnForNeo4jLabel(arn string) string {
// Replace non-allowed characters with underscores or other allowed characters
sanitized := strings.ReplaceAll(arn, ":", "_")
sanitized = strings.ReplaceAll(sanitized, "-", "_")
// Add more replacements if needed
return sanitized
}
+160 -109
View File
@@ -1,28 +1,36 @@
package aws
import (
"context"
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type IamPrincipalsModule struct {
// General configuration data
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
SkipAdminCheck bool
iamSimClient IamSimulatorModule
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
// Main module data
Users []User
@@ -41,6 +49,8 @@ type User struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}
type Group struct {
@@ -60,13 +70,16 @@ type Role struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}
func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "principals"
localAdminMap := make(map[string]bool)
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
@@ -76,69 +89,143 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
fmt.Printf("[%s][%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// wg := new(sync.WaitGroup)
// done := make(chan bool)
// go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, done)
// wg.Add(1)
// m.CommandCounter.Pending++
// m.CommandCounter.IncrPending()
//m.executeChecks(wg)
// wg.Wait()
// done <- true
// <-done
m.getIAMUsers()
m.getIAMRoles()
m.addIAMUsersToTable()
m.addIAMRolesToTable()
//fmt.Printf("\nAnalyzed Resources by Region\n\n")
m.output.Headers = []string{
"Service",
"Account",
"Type",
"Name",
"Arn",
"AttachedPolicies",
"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// "AttachedPolicies",
// "InlinePolicies",
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Type",
"Name",
"Arn",
//"AttachedPolicies",
//"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Type",
"Name",
"Arn",
// "AttachedPolicies",
// "InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
//Table rows
for i := range m.Users {
if m.pmapperError == nil {
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Users[i].Arn)
} else {
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Users[i].Arn, m.iamSimClient, localAdminMap)
}
m.output.Body = append(
m.output.Body,
[]string{
m.Users[i].AWSService,
aws.ToString(m.Caller.Account),
m.Users[i].Type,
m.Users[i].Name,
m.Users[i].Arn,
// m.Users[i].AttachedPolicies,
// m.Users[i].InlinePolicies,
strings.Join(m.Users[i].AttachedPolicies, " , "),
strings.Join(m.Users[i].InlinePolicies, " , "),
m.Users[i].Admin,
m.Users[i].CanPrivEsc,
},
)
}
for i := range m.Roles {
if m.pmapperError == nil {
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Roles[i].Arn)
} else {
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Roles[i].Arn, m.iamSimClient, localAdminMap)
}
m.output.Body = append(
m.output.Body,
[]string{
m.Roles[i].AWSService,
aws.ToString(m.Caller.Account),
m.Roles[i].Type,
m.Roles[i].Name,
m.Roles[i].Arn,
// m.Roles[i].AttachedPolicies,
// m.Roles[i].InlinePolicies,
strings.Join(m.Roles[i].AttachedPolicies, " , "),
strings.Join(m.Roles[i].InlinePolicies, " , "),
m.Roles[i].Admin,
m.Roles[i].CanPrivEsc,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s IAM principals found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -150,110 +237,74 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF APPLICABLE
func (m *IamPrincipalsModule) executeChecks(wg *sync.WaitGroup) {
defer wg.Done()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getIAMUsers()
m.getIAMRoles()
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
*/
func (m *IamPrincipalsModule) getIAMUsers() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
func (m *IamPrincipalsModule) addIAMUsersToTable() {
var AWSService = "IAM"
var IAMtype = "User"
var attachedPolicies []string
var inlinePolicies []string
for {
ListUsers, err := m.IAMClient.ListUsers(
context.TODO(),
&iam.ListUsersInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
for _, user := range ListUsers.Users {
arn := user.Arn
name := user.UserName
for _, user := range ListUsers {
arn := user.Arn
name := user.UserName
m.Users = append(
m.Users,
User{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
// Pagination control. After the last page of output, the for loop exits.
if ListUsers.Marker != nil {
PaginationControl = ListUsers.Marker
} else {
PaginationControl = nil
break
}
m.Users = append(
m.Users,
User{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
}
func (m *IamPrincipalsModule) getIAMRoles() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
func (m *IamPrincipalsModule) addIAMRolesToTable() {
//var totalRoles int
var AWSService = "IAM"
var IAMtype = "Role"
var attachedPolicies []string
var inlinePolicies []string
for {
ListRoles, err := m.IAMClient.ListRoles(
context.TODO(),
&iam.ListRolesInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, role := range ListRoles.Roles {
arn := role.Arn
name := role.RoleName
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
m.Roles = append(
m.Roles,
Role{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
for _, role := range ListRoles {
arn := role.Arn
name := role.RoleName
// Pagination control. After the last page of output, the for loop exits.
if ListRoles.Marker != nil {
PaginationControl = ListRoles.Marker
} else {
PaginationControl = nil
//fmt.Printf("IAM Roles: %d\n\n", totalRoles)
break
}
m.Roles = append(
m.Roles,
Role{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
}
+79 -24
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/internal"
@@ -20,12 +21,15 @@ type RAMModule struct {
// General configuration data
RAMClient *ram.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Resources []Resource
@@ -44,8 +48,8 @@ type Resource struct {
ShareType string
}
func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ram"
@@ -75,7 +79,7 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, dataReceiver)
}
@@ -91,7 +95,7 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Service",
"Account",
"Region",
"Share Name",
"Type",
@@ -99,12 +103,44 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
"Share Type",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Share Name",
"Type",
"Owner",
"Share Type",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Share Name",
"Type",
"Owner",
"Share Type",
}
}
// Table rows
for i := range m.Resources {
m.output.Body = append(
m.output.Body,
[]string{
m.Resources[i].AWSService,
aws.ToString(m.Caller.Account),
m.Resources[i].Region,
m.Resources[i].Name,
m.Resources[i].Type,
@@ -115,10 +151,25 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s resources found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No resources found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -128,20 +179,24 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
func (m *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Resource) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "EMBEDDED_IN_PACKAGE",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ram", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getRAMResourcesPerRegion(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
}
@@ -180,7 +235,7 @@ func (m *RAMModule) getRAMResourcesPerRegion(r string, dataReceiver chan Resourc
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -204,7 +259,7 @@ func (m *RAMModule) getRAMResourcesPerRegion(r string, dataReceiver chan Resourc
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
File diff suppressed because it is too large. Load diff
+262
View File
@@ -0,0 +1,262 @@
package aws
import (
"testing"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
func TestIsResourcePolicyInteresting(t *testing.T) {
testCases := []struct {
name string
input string
expected bool
}{
{
name: "test1",
input: "Everyone can sqs:SendMessage & can sqs:ReceiveMessage",
expected: true,
},
{
name: "PrincipalOrgPaths",
input: "aws:PrincipalOrgPaths",
expected: true,
},
{
name: "Empty",
input: "",
expected: false,
},
{
name: "NotInteresting",
input: "sns.amazonaws.com can lambda:InvokeFunction",
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := isResourcePolicyInteresting(tc.input)
if actual != tc.expected {
t.Errorf("Expected %v but got %v for input %s", tc.expected, actual, tc.input)
}
})
}
}
func TestKMSResourceTrusts(t *testing.T) {
mockedKMSClient := &sdk.MockedKMSClient{}
var kmsClient sdk.KMSClientInterface = mockedKMSClient
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: &kmsClient,
APIGatewayClient: nil,
EC2Client: nil,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "key1",
ARN: "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, true)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
}
}
}
func TestAPIGatewayResourceTrusts(t *testing.T) {
mockedAPIGatewayClient := &sdk.MockedAWSAPIGatewayClient{}
var apiGatewayClient sdk.APIGatewayClientInterface = mockedAPIGatewayClient
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: nil,
APIGatewayClient: &apiGatewayClient,
EC2Client: nil,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "api1",
ARN: "arn:aws:execute-api:us-west-2:123456789012:abcdefg/*",
Public: "No",
Interesting: "Yes",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, false)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
if expectedResource2.Public != tc.testModule.Resources2[index].Public {
t.Fatal("Resource Public does not match expected value")
}
if expectedResource2.Interesting != tc.testModule.Resources2[index].Interesting {
t.Fatal("Resource Interesting does not match expected value")
}
}
}
}
func TestVpcEndpointResourceTrusts(t *testing.T) {
mockedEC2Client := &sdk.MockedEC2Client2{}
var ec2Client sdk.AWSEC2ClientInterface = mockedEC2Client
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: nil,
APIGatewayClient: nil,
EC2Client: &ec2Client,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "vpce-1234567890abcdefg",
ARN: "vpce-1234567890abcdefg",
Public: "No",
},
{
Name: "vpce-1234567890abcdefh",
ARN: "vpce-1234567890abcdefh",
Public: "No",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, false)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
if expectedResource2.Public != tc.testModule.Resources2[index].Public {
t.Fatal("Resource Public does not match expected value")
}
}
}
}
func TestOpenSearchResourceTrusts(t *testing.T) {
mockedOpenSearchClient := &sdk.MockedOpenSearchClient{}
var openSearchClient sdk.OpenSearchClientInterface = mockedOpenSearchClient
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: nil,
APIGatewayClient: nil,
EC2Client: nil,
OpenSearchClient: &openSearchClient,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "domain1",
ARN: "arn:aws:es:us-east-1:123456789012:domain/domain1",
Public: "No",
},
{
Name: "domain2",
ARN: "arn:aws:es:us-east-1:123456789012:domain/domain2",
Public: "Yes",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, false)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
if expectedResource2.Public != tc.testModule.Resources2[index].Public {
t.Fatal("Resource Public does not match expected value")
}
}
}
}
+459 -224
View File
@@ -1,29 +1,25 @@
package aws
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/knownawsaccountslookup"
"github.com/sirupsen/logrus"
)
type RoleTrustsModule struct {
// General configuration data
IAMClientListRoles iam.ListRolesAPIClient
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
Caller sts.GetCallerIdentityOutput
@@ -32,12 +28,20 @@ type RoleTrustsModule struct {
CommandCounter internal.CommandCounter
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
AWSOutputType string
AWSTableCols string
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
iamSimClient IamSimulatorModule
// Main module data
AnalyzedRoles []AnalyzedRole
AnalyzedRoles []AnalyzedRole
RoleTrustTable []RoleTrustRow
vendors *knownawsaccountslookup.Vendors
// Used to store output data for pretty printing
output internal.OutputData2
@@ -45,49 +49,27 @@ type RoleTrustsModule struct {
modLog *logrus.Entry
}
type RoleTrustRow struct {
RoleARN string
RoleName string
TrustedPrincipal string
TrustedService string
TrustedFederatedProvider string
TrustedFederatedSubject string
ExternalID string
IsAdmin string
CanPrivEsc string
}
type AnalyzedRole struct {
roleARN *string
trustsDoc trustPolicyDocument
trustsDoc policy.TrustPolicyDocument
// trustType string // UNUSED FIELD, PLEASE REVIEW
Admin string
CanPrivEsc string
}
type trustPolicyDocument struct {
Version string `json:"Version"`
Statement []RoleTrustStatementEntry `json:"Statement"`
}
type RoleTrustStatementEntry struct {
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal struct {
AWS ListOfPrincipals `json:"AWS"`
Service ListOfPrincipals `json:"Service"`
Federated ListOfPrincipals `json:"Federated"`
} `json:"Principal"`
Action string `json:"Action"`
Condition struct {
StringEquals struct {
StsExternalID string `json:"sts:ExternalId"`
SAMLAud string `json:"SAML:aud"`
OidcEksSub string `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
} `json:"StringEquals"`
StringLike struct {
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
OidcEksSub string `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
} `json:"StringLike"`
ForAnyValueStringLike struct {
CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
} `json:"ForAnyValue:StringLike"`
} `json:"Condition"`
}
func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory string, verbosity int) {
func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "role-trusts"
@@ -98,16 +80,18 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
localAdminMap := make(map[string]bool)
fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
if m.pmapperError != nil {
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
} else {
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.vendors = knownawsaccountslookup.NewVendorMap()
m.vendors.PopulateKnownAWSAccounts()
}
fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
//fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// } else {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
m.getAllRoleTrusts()
if m.pmapperError == nil {
@@ -121,170 +105,476 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory
}
}
m.printPrincipalTrusts(outputFormat, outputDirectory)
m.printServiceTrusts(outputFormat, outputDirectory)
m.printFederatedTrusts(outputFormat, outputDirectory)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
principalsHeader, principalsBody, principalTableCols := m.printPrincipalTrusts(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: principalsHeader,
Body: principalsBody,
TableCols: principalTableCols,
Name: "role-trusts-principals",
})
rootPrincipalsHeader, rootPrincipalsBody, rootPrincipalTableCols := m.printPrincipalTrustsRootOnly(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: rootPrincipalsHeader,
Body: rootPrincipalsBody,
TableCols: rootPrincipalTableCols,
Name: "role-trusts-principals-root-trusts-without-external-id",
SkipPrintToScreen: true,
})
servicesHeader, servicesBody, serviceTableCols := m.printServiceTrusts(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: servicesHeader,
Body: servicesBody,
TableCols: serviceTableCols,
Name: "role-trusts-services",
})
federatedHeader, federatedBody, federatedTableCols := m.printFederatedTrusts(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: federatedHeader,
Body: federatedBody,
TableCols: federatedTableCols,
Name: "role-trusts-federated",
})
o.WriteFullOutput(o.Table.TableFiles, nil)
if len(principalsBody) > 0 {
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(principalsBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
if len(servicesBody) > 0 {
fmt.Printf("[%s][%s] %s service role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
if len(federatedBody) > 0 {
fmt.Printf("[%s][%s] %s federated role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) {
func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]string, [][]string, []string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-principals"
m.output.Headers = []string{
"Role",
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
for _, principal := range statement.Principal.AWS {
column1 := aws.ToString(role.roleARN)
column2 := principal
column3 := statement.Condition.StringEquals.StsExternalID
column4 := role.Admin
column5 := role.CanPrivEsc
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
//check to see if the accountID is known
if strings.Contains(principal, "arn:aws:iam::") || strings.Contains(principal, "root") {
accountID := strings.Split(principal, ":")[4]
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
if vendorName != "" {
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
}
}
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
// if there is more than one externalID concat them using newlines
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedPrincipal,
RoleTrustRow.ExternalID,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
////m.output.OutputSelector(outputFormat)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
m.sortTrustsTablePerTrustedPrincipal()
return header, body, tableCols
}
func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) {
// printPrincipalTrusts but only those that have a trusted principal that contains :root and also does not have an external ID
func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string) ([]string, [][]string, []string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-services"
m.output.Headers = []string{
"Role",
"Trusted Service",
m.output.FullFilename = "role-trusts-principals-root-trusts-without-external-id"
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
for _, service := range statement.Principal.Service {
column1 := aws.ToString(role.roleARN)
column2 := service
column3 := statement.Condition.StringEquals.StsExternalID
column4 := role.Admin
column5 := role.CanPrivEsc
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
for _, principal := range statement.Principal.AWS {
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == nil {
accountID := strings.Split(principal, ":")[4]
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
if vendorName != "" {
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
}
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedPrincipal,
RoleTrustRow.ExternalID,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
return header, body, tableCols
}
func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirectory string) {
func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string, [][]string, []string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-services"
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Service",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
for _, service := range statement.Principal.Service {
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedService: service,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedService,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
// sort the rows based on column 2 (service)
sort.SliceStable(body, func(i, j int) bool {
return body[i][3] < body[j][3]
})
return header, body, tableCols
}
func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]string, [][]string, []string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-federated"
var column3, column2 string
m.output.Headers = []string{
"Role",
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Provider",
"Trusted Subject",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
for _, role := range m.AnalyzedRoles {
column1 := aws.ToString(role.roleARN)
for _, statement := range role.trustsDoc.Statement {
if len(statement.Principal.Federated) > 0 {
column2, column3 = parseFederatedTrustPolicy(statement)
column4 := role.Admin
column5 := role.CanPrivEsc
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
provider, subjects := parseFederatedTrustPolicy(statement)
for _, subject := range subjects {
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedFederatedProvider: provider,
TrustedFederatedSubject: subject,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedFederatedProvider,
RoleTrustRow.TrustedFederatedSubject,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
return header, body, tableCols
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) {
var column2, column3 string
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
column2 = "GitHub Actions (" + statement.Principal.Federated[0] + ")"
trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n")
if trustedRepos == "" {
column3 = "ALL REPOS!!!"
func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, []string) {
var provider string
var subjects []string
if len(statement.Principal.Federated) > 1 {
sharedLogger.Warnf("Multiple federated providers found in the trust policy. This is not currently supported. Please review the trust policy for specifics.")
provider = "Multiple Federated Providers"
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
}
switch {
// lets use the Federated field to determine the provider, then based on the provider we can grab the list of subjects
case strings.Contains(statement.Principal.Federated[0], "token.actions.githubusercontent.com"):
provider = "GitHub"
if len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.TokenActionsGithubusercontentComSub...)
} else if len(statement.Condition.StringEquals.TokenActionsGithubusercontentComSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.TokenActionsGithubusercontentComSub...)
} else if len(statement.Condition.ForAllValuesStringLike.TokenActionsGithubusercontentComSub) > 0 {
subjects = append(subjects, statement.Condition.ForAllValuesStringLike.TokenActionsGithubusercontentComSub...)
} else {
column3 = trustedRepos
subjects = append(subjects, "ALL REPOS!!!")
}
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
column2 = "AWS SSO (" + statement.Principal.Federated[0] + ")"
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
column2 = "Okta (" + statement.Principal.Federated[0] + ")"
}
column3 = "Not applicable"
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" {
column2 = "EKS (" + statement.Principal.Federated[0] + ")"
if statement.Condition.StringEquals.OidcEksSub != "" {
column3 = statement.Condition.StringEquals.OidcEksSub
} else if statement.Condition.StringLike.OidcEksSub != "" {
column3 = statement.Condition.StringLike.OidcEksSub
case strings.Contains(statement.Principal.Federated[0], "oidc.eks"):
// extract accountId from statement.Principal.Federated[0]
accountId := strings.Split(statement.Principal.Federated[0], ":")[4]
provider = fmt.Sprintf("EKS-%s", accountId)
//provider = "EKS"
//provider = statement.Principal.Federated[0]
if len(statement.Condition.StringLike.OidcEksSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.OidcEksSub...)
} else if len(statement.Condition.StringEquals.OidcEksSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.OidcEksSub...)
} else {
column3 = "ALL SERVICE ACCOUNTS!"
subjects = append(subjects, "ALL SERVICE ACCOUNTS!!!")
}
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
column2 = "Cognito (" + statement.Principal.Federated[0] + ")"
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR
// terraform case
case strings.Contains(statement.Principal.Federated[0], "app.terraform.io"):
provider = "Terraform Cloud"
if len(statement.Condition.StringLike.TerraformSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.TerraformSub...)
} else if len(statement.Condition.StringEquals.TerraformSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.TerraformSub...)
} else {
subjects = append(subjects, "ALL WORKSPACES")
}
} else {
if column2 == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
column2 = "EKS (" + statement.Principal.Federated[0] + ")"
column3 = "ALL SERVICE ACCOUNTS!"
} else if column2 == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
column2 = "AWS SSO (" + statement.Principal.Federated[0] + ")"
// Azure AD case
case strings.Contains(statement.Principal.Federated[0], "http://sts.windows.net"):
provider = "Azure AD"
if len(statement.Condition.StringLike.AzureADIss) > 0 {
subjects = append(subjects, statement.Condition.StringLike.AzureADIss...)
} else if len(statement.Condition.StringEquals.AzureADIss) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.AzureADIss...)
} else {
subjects = append(subjects, "ALL ISSUERS")
}
///AWS SSO case
case strings.Contains(statement.Principal.Federated[0], "AWSSSO"):
//provider = "AWS SSO"
accountId := strings.Split(statement.Principal.Federated[0], ":")[4]
provider = fmt.Sprintf("AWSSSO-%s", accountId)
subjects = append(subjects, "Not applicable")
// okta case
case strings.Contains(statement.Principal.Federated[0], "Okta"):
provider = "Okta"
subjects = append(subjects, "Not applicable")
// cognito case
case statement.Principal.Federated[0] == "cognito-identity.amazonaws.com":
provider = "Cognito"
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR)
} else {
subjects = append(subjects, "ALL IDENTITIES")
}
// google workspace case
case strings.Contains(statement.Principal.Federated[0], "workspace.google.com"):
provider = "Google Workspace"
if len(statement.Condition.StringLike.GoogleWorkspaceSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.GoogleWorkspaceSub...)
} else if len(statement.Condition.StringEquals.GoogleWorkspaceSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.GoogleWorkspaceSub...)
} else {
subjects = append(subjects, "ALL USERS")
}
// GCP case
case strings.Contains(statement.Principal.Federated[0], "accounts.google.com"):
provider = "GCP"
if len(statement.Condition.StringLike.GCPSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.GCPSub...)
} else if len(statement.Condition.StringEquals.GCPSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.GCPSub...)
} else {
subjects = append(subjects, "ALL USERS")
}
// auth0 case
//not ready yet
// case strings.Contains(statement.Principal.Federated[0], "auth0.com"):
// provider = "Auth0"
// if len(statement.Condition.ForAnyValueStringLike.Auth0Amr) > 0 {
// subjects = append(subjects, statement.Condition.ForAnyValueStringLike.Auth0Amr...)
// } else {
// subjects = append(subjects, "ALL GROUPS")
// }
// circleci case
case strings.Contains(statement.Principal.Federated[0], "oidc.circleci.com"):
provider = "CircleCI"
if len(statement.Condition.StringLike.CircleCIAud) > 0 {
subjects = append(subjects, statement.Condition.StringLike.CircleCIAud...)
} else if len(statement.Condition.StringEquals.CircleCIAud) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.CircleCIAud...)
} else {
subjects = append(subjects, "ALL PROJECTS")
}
case strings.Contains(statement.Principal.Federated[0], "saml-provider"):
// the provider name is the last part of the ARN
provider = strings.Split(statement.Principal.Federated[0], ":saml-provider/")[1]
subjects = append(subjects, "Not applicable")
default:
provider = "Unknown Federated Provider"
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
}
return column2, column3
return provider, subjects
}
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
@@ -297,85 +587,30 @@ func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
}
func (m *RoleTrustsModule) getAllRoleTrusts() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
}
// This for loop exits at the end depending on whether the output hits its last page (see pagination control block at the end of the loop).
for {
results, err := m.IAMClientListRoles.ListRoles(
context.TODO(),
&iam.ListRolesInput{
Marker: PaginationMarker,
},
)
for _, role := range ListRoles {
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, role := range results.Roles {
trustsdoc, err := parseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
if role.Arn != nil {
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{
roleARN: role.Arn,
trustsDoc: trustsdoc,
Admin: "",
CanPrivEsc: "",
})
}
if role.Arn != nil {
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{
roleARN: role.Arn,
trustsDoc: trustsdoc,
Admin: "",
CanPrivEsc: "",
})
}
// Pagination control. After the last page of output, the for loop exits.
if results.IsTruncated {
PaginationMarker = results.Marker
} else {
PaginationMarker = nil
break
}
}
}
func parseRoleTrustPolicyDocument(role types.Role) (trustPolicyDocument, error) {
document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument))
// These next six lines are a hack, needed because the EKS OIDC json field name is dynamic
// and therefore can't be used to unmarshall in a predictable way. The hack involves replacing
// the random pattern with a predictable one so that we can add the predictable one in the struct
// used to unmarshall.
pattern := `(\w+)\:`
pattern2 := `".[a-zA-Z0-9\-\.]+/id/`
var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub")
var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud")
document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub")
document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud")
var parsedDocumentToJSON trustPolicyDocument
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
return parsedDocumentToJSON, nil
}
// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
type ListOfPrincipals []string
func (r *ListOfPrincipals) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err == nil {
*r = append(*r, s)
return nil
}
var ss []string
if err := json.Unmarshal(b, &ss); err == nil {
*r = ss
return nil
}
return errors.New("cannot unmarshal neither to a string nor a slice of strings")
}
+96 -59
View File
@@ -1,26 +1,28 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type Route53Module struct {
// General configuration data
Route53Client *route53.Client
Route53Client sdk.AWSRoute53ClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
@@ -42,9 +44,9 @@ type Record struct {
PrivateZone string
}
func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string, verbosity int) {
func (m *Route53Module) PrintRoute53(outputDirectory string, verbosity int) {
// These stuct values are used by the output module
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "route53"
@@ -60,19 +62,48 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string
m.getRoute53Records()
m.output.Headers = []string{
"Service",
"Account",
"Name",
"Type",
"Value",
"PrivateZone",
}
// If the user specified table columns, use those.
// If the user specified -o wide, use the wide default cols for this module.
// Otherwise, use the hardcoded default cols for this module.
var tableCols []string
// If the user specified table columns, use those.
if m.AWSTableCols != "" {
// If the user specified wide as the output format, use these columns.
// remove any spaces between any commas and the first letter after the commas
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
tableCols = strings.Split(m.AWSTableCols, ",")
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Name",
"Type",
"Value",
"PrivateZone",
}
} else {
tableCols = []string{
"Name",
"Type",
"Value",
"PrivateZone",
}
}
// Table rows
for i := range m.Records {
m.output.Body = append(
m.output.Body,
[]string{
m.Records[i].AWSService,
aws.ToString(m.Caller.Account),
m.Records[i].Name,
m.Records[i].Type,
m.Records[i].Value,
@@ -83,11 +114,25 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
m.writeLoot(m.output.FilePath, verbosity)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s DNS records found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
@@ -101,7 +146,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
route53ARecordsPublicZonesFileName := filepath.Join(path, "route53-A-records-public-Zones.txt")
@@ -111,7 +156,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
var route53APublicRecords string
for _, record := range m.Records {
if record.Type == "A" || record.Type == "AAAA" {
if record.Type == "A" || record.Type == "AAAA" || record.Type == "CNAME" {
if record.PrivateZone == "True" {
route53APrivateRecords = route53APrivateRecords + fmt.Sprintln(record.Name)
} else {
@@ -123,7 +168,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(route53ARecordsPublicZonesFileName, []byte(route53APublicRecords), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
@@ -141,7 +186,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(route53ARecordsPrivateZonesFileName, []byte(route53APrivateRecords), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
@@ -160,48 +205,47 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
func (m *Route53Module) getRoute53Records() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var recordName string
var recordType string
for {
ListHostedZones, err := m.Route53Client.ListHostedZones(
context.TODO(),
&route53.ListHostedZonesInput{
Marker: PaginationControl,
},
)
HostedZones, err := sdk.CachedRoute53ListHostedZones(m.Route53Client, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.IncrError()
return
}
var privateZone string
for _, zone := range HostedZones {
if zone.Config.PrivateZone {
privateZone = "True"
} else {
privateZone = "False"
}
Records, err := sdk.CachedRoute53ListResourceRecordSets(m.Route53Client, aws.ToString(m.Caller.Account), aws.ToString(zone.Id))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, record := range Records {
recordName = aws.ToString(record.Name)
recordType = string(record.Type)
var privateZone string
for _, zone := range ListHostedZones.HostedZones {
id := aws.ToString(zone.Id)
if zone.Config.PrivateZone {
privateZone = "True"
if record.AliasTarget != nil {
m.Records = append(
m.Records,
Record{
AWSService: "Route53",
Name: recordName,
Type: fmt.Sprintf("Alias[%s]", recordType),
Value: aws.ToString(record.AliasTarget.DNSName),
PrivateZone: privateZone,
})
} else {
privateZone = "False"
}
ListResourceRecordSets, err := m.Route53Client.ListResourceRecordSets(
context.TODO(),
&route53.ListResourceRecordSetsInput{
HostedZoneId: &id,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, record := range ListResourceRecordSets.ResourceRecordSets {
recordName = aws.ToString(record.Name)
recordType = string(record.Type)
for _, resourceRecord := range record.ResourceRecords {
recordValue := resourceRecord.Value
m.Records = append(
@@ -213,18 +257,11 @@ func (m *Route53Module) getRoute53Records() {
Value: aws.ToString(recordValue),
PrivateZone: privateZone,
})
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if ListHostedZones.NextMarker != nil {
PaginationControl = ListHostedZones.NextMarker
} else {
PaginationControl = nil
break
}
}
}
+516
View File
@@ -0,0 +1,516 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apigateway"
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type APIGatewayClientInterface interface {
GetRestApis(context.Context, *apigateway.GetRestApisInput, ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error)
GetStages(context.Context, *apigateway.GetStagesInput, ...func(*apigateway.Options)) (*apigateway.GetStagesOutput, error)
GetResources(context.Context, *apigateway.GetResourcesInput, ...func(*apigateway.Options)) (*apigateway.GetResourcesOutput, error)
GetDomainNames(context.Context, *apigateway.GetDomainNamesInput, ...func(*apigateway.Options)) (*apigateway.GetDomainNamesOutput, error)
GetBasePathMappings(context.Context, *apigateway.GetBasePathMappingsInput, ...func(*apigateway.Options)) (*apigateway.GetBasePathMappingsOutput, error)
GetMethod(context.Context, *apigateway.GetMethodInput, ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error)
GetUsagePlans(context.Context, *apigateway.GetUsagePlansInput, ...func(*apigateway.Options)) (*apigateway.GetUsagePlansOutput, error)
GetUsagePlanKeys(context.Context, *apigateway.GetUsagePlanKeysInput, ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error)
}
type CachedGetMethodOutput struct {
ApiKeyRequired *bool
AuthorizationScopes []string
AuthorizationType *string
AuthorizerId *string
HttpMethod *string
MethodIntegration *apiGatewayTypes.Integration
MethodResponses map[string]apiGatewayTypes.MethodResponse
OperationName *string
RequestModels map[string]string
RequestParameters map[string]bool
RequestValidatorId *string
ResourceId *string
}
type CachedGetStagesOutput struct {
Item []apiGatewayTypes.Stage
}
func init() {
gob.Register([]apiGatewayTypes.RestApi{})
gob.Register(CachedGetStagesOutput{})
gob.Register([]apiGatewayTypes.Resource{})
gob.Register([]apiGatewayTypes.DomainName{})
gob.Register([]apiGatewayTypes.BasePathMapping{})
gob.Register(CachedGetMethodOutput{})
gob.Register([]apiGatewayTypes.UsagePlan{})
gob.Register([]apiGatewayTypes.UsagePlanKey{})
}
func IsPublicApiGateway(ra *apiGatewayTypes.RestApi) bool {
for _, endpointType := range ra.EndpointConfiguration.Types {
if endpointType == apiGatewayTypes.EndpointTypeRegional || endpointType == apiGatewayTypes.EndpointTypeEdge {
return true
}
}
return false
}
// CachedApiGatewayGetRestAPIs function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination
func CachedApiGatewayGetRestAPIs(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.RestApi, error) {
var PaginationControl *string
var restAPIs []apiGatewayTypes.RestApi
cacheKey := fmt.Sprintf("%s-apigateway-GetRestApis-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetRestApis",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.RestApi), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetRestApis",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetRestApis, err := client.GetRestApis(
context.TODO(),
&apigateway.GetRestApisInput{
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return restAPIs, err
}
restAPIs = append(restAPIs, GetRestApis.Items...)
//pagination
if GetRestApis.Position == nil {
break
}
PaginationControl = GetRestApis.Position
}
internal.Cache.Set(cacheKey, restAPIs, cache.DefaultExpiration)
return restAPIs, nil
}
// create a CachedApiGatewayGetStages function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID string, region string, restAPIID string) (*apigateway.GetStagesOutput, error) {
cacheKey := fmt.Sprintf("%s-apigateway-GetStages-%s-%s", accountID, region, restAPIID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetStages",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "hit",
}).Info("AWS API call")
// Convert cached data back to GetStagesOutput before returning
cachedOutput := cached.(*CachedGetStagesOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetStagesOutput(cachedOutput), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetStages",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "miss",
}).Info("AWS API call")
GetStages, err := client.GetStages(
context.TODO(),
&apigateway.GetStagesInput{
RestApiId: &restAPIID,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return &apigateway.GetStagesOutput{}, err
}
// Convert GetStagesOutput to CachedGetStagesOutput before caching
cachedVersion := toCachedGetStagesOutput(GetStages)
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
return GetStages, err
}
// Convert from AWS SDK type to custom type for caching
func toCachedGetStagesOutput(gso *apigateway.GetStagesOutput) *CachedGetStagesOutput {
return &CachedGetStagesOutput{
Item: gso.Item,
}
}
// Convert back to AWS SDK type after fetching from cache
func fromCachedGetStagesOutput(cgso *CachedGetStagesOutput) *apigateway.GetStagesOutput {
return &apigateway.GetStagesOutput{
Item: cgso.Item,
// Initialize ResultMetadata or leave it as zero value if it's not required for your use case
}
}
// create a CachedApiGatewayGetResources function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
func CachedApiGatewayGetResources(client APIGatewayClientInterface, accountID string, region string, restAPIID string) ([]apiGatewayTypes.Resource, error) {
var PaginationControl *string
var resources []apiGatewayTypes.Resource
cacheKey := fmt.Sprintf("%s-apigateway-GetResources-%s-%s", accountID, region, restAPIID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetResources",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.Resource), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetResources",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "miss",
}).Info("AWS API call")
for {
GetResources, err := client.GetResources(
context.TODO(),
&apigateway.GetResourcesInput{
RestApiId: &restAPIID,
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return resources, err
}
resources = append(resources, GetResources.Items...)
//pagination
if GetResources.Position == nil {
break
}
PaginationControl = GetResources.Position
}
internal.Cache.Set(cacheKey, resources, cache.DefaultExpiration)
return resources, nil
}
// create a CachedApiGatewayGetDomainNames function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetDomainNames(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.DomainName, error) {
var PaginationControl *string
var domainNames []apiGatewayTypes.DomainName
cacheKey := fmt.Sprintf("%s-apigateway-GetDomainNames-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetDomainNames",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.DomainName), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetDomainNames",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetDomainNames, err := client.GetDomainNames(
context.TODO(),
&apigateway.GetDomainNamesInput{
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return domainNames, err
}
domainNames = append(domainNames, GetDomainNames.Items...)
//pagination
if GetDomainNames.Position == nil {
break
}
PaginationControl = GetDomainNames.Position
}
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
return domainNames, nil
}
// create a CachedApiGatewayGetBasePathMappings function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetBasePathMappings(client APIGatewayClientInterface, accountID string, region string, domain *string) ([]apiGatewayTypes.BasePathMapping, error) {
var PaginationControl *string
var basePathMappings []apiGatewayTypes.BasePathMapping
cacheKey := fmt.Sprintf("%s-apigateway-GetBasePathMappings-%s-%s", accountID, region, aws.ToString(domain))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetBasePathMappings",
"account": accountID,
"region": region,
"domainName": aws.ToString(domain),
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.BasePathMapping), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetBasePathMappings",
"account": accountID,
"region": region,
"domainName": aws.ToString(domain),
"cache": "miss",
}).Info("AWS API call")
for {
GetBasePathMappings, err := client.GetBasePathMappings(
context.TODO(),
&apigateway.GetBasePathMappingsInput{
DomainName: domain,
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return basePathMappings, err
}
basePathMappings = append(basePathMappings, GetBasePathMappings.Items...)
//pagination
if GetBasePathMappings.Position == nil {
break
}
PaginationControl = GetBasePathMappings.Position
}
internal.Cache.Set(cacheKey, basePathMappings, cache.DefaultExpiration)
return basePathMappings, nil
}
// create a CachedApiGatewayGetMethod function that accepts a client, account id, region, rest api id, and resource id. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID string, region string, restAPIID string, resourceID string, method string) (*apigateway.GetMethodOutput, error) {
cacheKey := fmt.Sprintf("%s-apigateway-GetMethod-%s-%s-%s-%s", accountID, region, restAPIID, resourceID, method)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetMethod",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"resourceId": resourceID,
"httpMethod": method,
"cache": "hit",
}).Info("AWS API call")
// Convert cached data back to GetMethodOutput before returning
cachedOutput := cached.(*CachedGetMethodOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetMethodOutput(cachedOutput), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetMethod",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"resourceId": resourceID,
"httpMethod": method,
"cache": "miss",
}).Info("AWS API call")
GetMethod, err := client.GetMethod(
context.TODO(),
&apigateway.GetMethodInput{
RestApiId: &restAPIID,
ResourceId: &resourceID,
HttpMethod: &method,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return &apigateway.GetMethodOutput{}, err
}
// Convert GetMethodOutput to CachedGetMethodOutput before caching
cachedVersion := toCachedGetMethodOutput(GetMethod)
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
return GetMethod, nil
}
// Convert from AWS SDK type to custom type for caching
func toCachedGetMethodOutput(gmo *apigateway.GetMethodOutput) *CachedGetMethodOutput {
return &CachedGetMethodOutput{
ApiKeyRequired: gmo.ApiKeyRequired,
AuthorizationScopes: gmo.AuthorizationScopes,
AuthorizationType: gmo.AuthorizationType,
AuthorizerId: gmo.AuthorizerId,
HttpMethod: gmo.HttpMethod,
MethodIntegration: gmo.MethodIntegration,
MethodResponses: gmo.MethodResponses,
OperationName: gmo.OperationName,
RequestModels: gmo.RequestModels,
RequestParameters: gmo.RequestParameters,
RequestValidatorId: gmo.RequestValidatorId,
}
}
// Convert back to AWS SDK type after fetching from cache
func fromCachedGetMethodOutput(cgmo *CachedGetMethodOutput) *apigateway.GetMethodOutput {
return &apigateway.GetMethodOutput{
ApiKeyRequired: cgmo.ApiKeyRequired,
AuthorizationScopes: cgmo.AuthorizationScopes,
AuthorizationType: cgmo.AuthorizationType,
AuthorizerId: cgmo.AuthorizerId,
HttpMethod: cgmo.HttpMethod,
MethodIntegration: cgmo.MethodIntegration,
MethodResponses: cgmo.MethodResponses,
OperationName: cgmo.OperationName,
RequestModels: cgmo.RequestModels,
RequestParameters: cgmo.RequestParameters,
RequestValidatorId: cgmo.RequestValidatorId,
// ResultMetadata: This will be missing or zero value; handle accordingly
}
}
// create a CachedApiGatewayGetUsagePlans function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetUsagePlans(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.UsagePlan, error) {
var PaginationControl *string
var usagePlans []apiGatewayTypes.UsagePlan
cacheKey := fmt.Sprintf("%s-apigateway-GetUsagePlans-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlans",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.UsagePlan), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlans",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetUsagePlans, err := client.GetUsagePlans(
context.TODO(),
&apigateway.GetUsagePlansInput{
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return usagePlans, err
}
usagePlans = append(usagePlans, GetUsagePlans.Items...)
//pagination
if GetUsagePlans.Position == nil {
break
}
PaginationControl = GetUsagePlans.Position
}
internal.Cache.Set(cacheKey, usagePlans, cache.DefaultExpiration)
return usagePlans, nil
}
// create a CachedApiGatewayGetUsagePlanKeys function that accepts a client, account id, region, and usage plan id. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetUsagePlanKeys(client APIGatewayClientInterface, accountID string, region string, usagePlanID string) ([]apiGatewayTypes.UsagePlanKey, error) {
var PaginationControl *string
var usagePlanKeys []apiGatewayTypes.UsagePlanKey
cacheKey := fmt.Sprintf("%s-apigateway-GetUsagePlanKeys-%s-%s", accountID, region, usagePlanID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlanKeys",
"account": accountID,
"region": region,
"usagePlanId": usagePlanID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.UsagePlanKey), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlanKeys",
"account": accountID,
"region": region,
"usagePlanId": usagePlanID,
"cache": "miss",
}).Info("AWS API call")
for {
GetUsagePlanKeys, err := client.GetUsagePlanKeys(
context.TODO(),
&apigateway.GetUsagePlanKeysInput{
UsagePlanId: &usagePlanID,
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return usagePlanKeys, err
}
usagePlanKeys = append(usagePlanKeys, GetUsagePlanKeys.Items...)
//pagination
if GetUsagePlanKeys.Position == nil {
break
}
PaginationControl = GetUsagePlanKeys.Position
}
internal.Cache.Set(cacheKey, usagePlanKeys, cache.DefaultExpiration)
return usagePlanKeys, nil
}
+143
View File
@@ -0,0 +1,143 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apigateway"
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
)
type MockedAWSAPIGatewayClient struct {
}
func (m *MockedAWSAPIGatewayClient) GetRestApis(ctx context.Context, input *apigateway.GetRestApisInput, options ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error) {
return &apigateway.GetRestApisOutput{
Items: []apiGatewayTypes.RestApi{
{
Id: aws.String("abcdefg"),
Name: aws.String("api1"),
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
Types: []apiGatewayTypes.EndpointType{
apiGatewayTypes.EndpointTypePrivate,
},
},
Policy: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"execute-api:Invoke\",\"Resource\":\"arn:aws:execute-api:us-west-2:123456789012:abcdefg/*/*/*\"}]}"),
},
{
Id: aws.String("qwerty"),
Name: aws.String("api2"),
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
Types: []apiGatewayTypes.EndpointType{
apiGatewayTypes.EndpointTypeRegional,
},
},
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetStages(ctx context.Context, input *apigateway.GetStagesInput, options ...func(*apigateway.Options)) (*apigateway.GetStagesOutput, error) {
return &apigateway.GetStagesOutput{
Item: []apiGatewayTypes.Stage{
{
StageName: aws.String("stage1"),
},
{
StageName: aws.String("stage2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetResources(ctx context.Context, input *apigateway.GetResourcesInput, options ...func(*apigateway.Options)) (*apigateway.GetResourcesOutput, error) {
return &apigateway.GetResourcesOutput{
Items: []apiGatewayTypes.Resource{
{
Id: aws.String("resource1"),
Path: aws.String("/path1"),
ResourceMethods: map[string]apiGatewayTypes.Method{
"GET": {
ApiKeyRequired: aws.Bool(true),
AuthorizerId: aws.String("authorizer1"),
OperationName: aws.String("operation1"),
},
},
},
{
Id: aws.String("resource2"),
Path: aws.String("/path2"),
ResourceMethods: map[string]apiGatewayTypes.Method{
"ANY": {
ApiKeyRequired: aws.Bool(true),
AuthorizerId: aws.String("authorizer2"),
OperationName: aws.String("operation2"),
},
},
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetDomainNames(ctx context.Context, input *apigateway.GetDomainNamesInput, options ...func(*apigateway.Options)) (*apigateway.GetDomainNamesOutput, error) {
return &apigateway.GetDomainNamesOutput{
Items: []apiGatewayTypes.DomainName{},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetBasePathMappings(ctx context.Context, input *apigateway.GetBasePathMappingsInput, options ...func(*apigateway.Options)) (*apigateway.GetBasePathMappingsOutput, error) {
return &apigateway.GetBasePathMappingsOutput{
Items: []apiGatewayTypes.BasePathMapping{
{
BasePath: aws.String("basepath1"),
RestApiId: aws.String("abcdefg"),
Stage: aws.String("stage1"),
},
{
BasePath: aws.String("basepath2"),
RestApiId: aws.String("qwerty"),
Stage: aws.String("stage2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetMethod(ctx context.Context, input *apigateway.GetMethodInput, options ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error) {
return &apigateway.GetMethodOutput{
ApiKeyRequired: aws.Bool(true),
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetUsagePlans(ctx context.Context, input *apigateway.GetUsagePlansInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlansOutput, error) {
return &apigateway.GetUsagePlansOutput{
Items: []apiGatewayTypes.UsagePlan{
{
Id: aws.String("usageplan1"),
Name: aws.String("usageplan1"),
ApiStages: []apiGatewayTypes.ApiStage{
{
ApiId: aws.String("abcdefg"),
Stage: aws.String("stage2"),
},
},
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetUsagePlanKeys(ctx context.Context, input *apigateway.GetUsagePlanKeysInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error) {
return &apigateway.GetUsagePlanKeysOutput{
Items: []apiGatewayTypes.UsagePlanKey{
{
Id: aws.String("usageplankey1"),
Type: aws.String("API_KEY"),
Value: aws.String("23oieuwefo3rfs"),
},
{
Id: aws.String("usageplankey2"),
Type: aws.String("API_KEY"),
Value: aws.String("982yf98fdv8dlds"),
},
},
}, nil
}
+281
View File
@@ -0,0 +1,281 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type APIGatewayv2ClientInterface interface {
GetApis(context.Context, *apigatewayv2.GetApisInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error)
GetDomainNames(context.Context, *apigatewayv2.GetDomainNamesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetDomainNamesOutput, error)
GetApiMappings(context.Context, *apigatewayv2.GetApiMappingsInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApiMappingsOutput, error)
GetStages(context.Context, *apigatewayv2.GetStagesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetStagesOutput, error)
GetRoutes(context.Context, *apigatewayv2.GetRoutesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetRoutesOutput, error)
}
type domainName apiGatwayV2Types.DomainName
type stage apiGatwayV2Types.Stage
func init() {
gob.Register([]apiGatwayV2Types.Api{})
//need to do this to avoid conflicts with the Instance type in the ec2 package
gob.Register([]domainName{})
gob.Register([]apiGatwayV2Types.ApiMapping{})
gob.Register([]stage{})
gob.Register([]apiGatwayV2Types.Route{})
}
func CachedAPIGatewayv2GetAPIs(client APIGatewayv2ClientInterface, accountID string, region string) ([]apiGatwayV2Types.Api, error) {
var PaginationControl *string
var apis []apiGatwayV2Types.Api
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApis-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApis",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.Api), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApis",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetApis, err := client.GetApis(
context.TODO(),
&apigatewayv2.GetApisInput{
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return apis, err
}
apis = append(apis, GetApis.Items...)
//pagination
if GetApis.NextToken == nil {
break
}
PaginationControl = GetApis.NextToken
}
internal.Cache.Set(cacheKey, apis, cache.DefaultExpiration)
return apis, nil
}
func CachedAPIGatewayv2GetDomainNames(client APIGatewayv2ClientInterface, accountID string, region string) ([]domainName, error) {
var PaginationControl *string
var domainNames []domainName
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetDomainNames-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetDomainNames",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]domainName), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetDomainNames",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetDomainNames, err := client.GetDomainNames(
context.TODO(),
&apigatewayv2.GetDomainNamesInput{
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return domainNames, err
}
// convert []apiGatwayV2Types.DomainName to []domainName
// Convert each element to domainName before appending to avoid conflicting with apigateway's domainname type
for _, item := range GetDomainNames.Items {
domainNames = append(domainNames, domainName(item))
}
//pagination
if GetDomainNames.NextToken == nil {
break
}
PaginationControl = GetDomainNames.NextToken
}
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
return domainNames, nil
}
func CachedAPIGatewayv2GetApiMappings(client APIGatewayv2ClientInterface, accountID string, region string, domain string) ([]apiGatwayV2Types.ApiMapping, error) {
var PaginationControl *string
var apiMappings []apiGatwayV2Types.ApiMapping
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApiMappings-%s-%s", accountID, region, domain)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApiMappings",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.ApiMapping), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApiMappings",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetApiMappings, err := client.GetApiMappings(
context.TODO(),
&apigatewayv2.GetApiMappingsInput{
DomainName: &domain,
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return apiMappings, err
}
apiMappings = append(apiMappings, GetApiMappings.Items...)
//pagination
if GetApiMappings.NextToken == nil {
break
}
PaginationControl = GetApiMappings.NextToken
}
internal.Cache.Set(cacheKey, apiMappings, cache.DefaultExpiration)
return apiMappings, nil
}
func CachedAPIGatewayv2GetStages(client APIGatewayv2ClientInterface, accountID string, region string, apiID string) ([]apiGatwayV2Types.Stage, error) {
var PaginationControl *string
var stages []apiGatwayV2Types.Stage
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetStages-%s-%s", accountID, region, apiID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetStages",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.Stage), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetStages",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetStages, err := client.GetStages(
context.TODO(),
&apigatewayv2.GetStagesInput{
ApiId: &apiID,
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return stages, err
}
stages = append(stages, GetStages.Items...)
//pagination
if GetStages.NextToken == nil {
break
}
PaginationControl = GetStages.NextToken
}
internal.Cache.Set(cacheKey, stages, cache.DefaultExpiration)
return stages, nil
}
func CachedAPIGatewayv2GetRoutes(client APIGatewayv2ClientInterface, accountID string, region string, apiID string) ([]apiGatwayV2Types.Route, error) {
var PaginationControl *string
var routes []apiGatwayV2Types.Route
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetRoutes-%s-%s", accountID, region, apiID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetRoutes",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.Route), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetRoutes",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetRoutes, err := client.GetRoutes(
context.TODO(),
&apigatewayv2.GetRoutesInput{
ApiId: &apiID,
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return routes, err
}
routes = append(routes, GetRoutes.Items...)
//pagination
if GetRoutes.NextToken == nil {
break
}
PaginationControl = GetRoutes.NextToken
}
internal.Cache.Set(cacheKey, routes, cache.DefaultExpiration)
return routes, nil
}
+83
View File
@@ -0,0 +1,83 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
)
type MockedAWSAPIGatewayv2Client struct {
}
func (m *MockedAWSAPIGatewayv2Client) GetApis(ctx context.Context, input *apigatewayv2.GetApisInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error) {
return &apigatewayv2.GetApisOutput{
Items: []apiGatwayV2Types.Api{
{
ApiId: aws.String("asdfsdfasdf"),
Name: aws.String("api1"),
ApiEndpoint: aws.String("https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com"),
},
{
ApiId: aws.String("qwertyqwerty"),
Name: aws.String("api2"),
ApiEndpoint: aws.String("https://qwertyqwerty.execute-api.us-east-1.amazonaws.com"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetDomainNames(ctx context.Context, input *apigatewayv2.GetDomainNamesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetDomainNamesOutput, error) {
return &apigatewayv2.GetDomainNamesOutput{
Items: []apiGatwayV2Types.DomainName{
{
DomainName: aws.String("domain1"),
},
{
DomainName: aws.String("domain2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetApiMappings(ctx context.Context, input *apigatewayv2.GetApiMappingsInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApiMappingsOutput, error) {
return &apigatewayv2.GetApiMappingsOutput{
Items: []apiGatwayV2Types.ApiMapping{
{
ApiMappingId: aws.String("apiMapping1"),
},
{
ApiMappingId: aws.String("apiMapping2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetStages(ctx context.Context, input *apigatewayv2.GetStagesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetStagesOutput, error) {
return &apigatewayv2.GetStagesOutput{
Items: []apiGatwayV2Types.Stage{
{
StageName: aws.String("stage1"),
},
{
StageName: aws.String("stage2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetRoutes(ctx context.Context, input *apigatewayv2.GetRoutesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetRoutesOutput, error) {
return &apigatewayv2.GetRoutesOutput{
Items: []apiGatwayV2Types.Route{
{
RouteId: aws.String("route1"),
RouteKey: aws.String("POST /route1"),
},
{
RouteId: aws.String("route2"),
RouteKey: aws.String("GET /route2"),
},
},
}, nil
}
+70
View File
@@ -0,0 +1,70 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/apprunner"
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AppRunnerClientInterface interface {
ListServices(context.Context, *apprunner.ListServicesInput, ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error)
}
func init() {
gob.Register([]apprunnerTypes.Service{})
gob.Register([]apprunnerTypes.ServiceSummary{})
}
func CachedAppRunnerListServices(client AppRunnerClientInterface, accountID string, region string) ([]apprunnerTypes.ServiceSummary, error) {
var PaginationControl *string
var services []apprunnerTypes.ServiceSummary
cacheKey := fmt.Sprintf("%s-apprunner-ListServices-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apprunner:ListServices",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apprunnerTypes.ServiceSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apprunner:ListServices",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListServices, err := client.ListServices(
context.TODO(),
&apprunner.ListServicesInput{
NextToken: PaginationControl,
},
func(o *apprunner.Options) {
o.Region = region
},
)
if err != nil {
return services, err
}
services = append(services, ListServices.ServiceSummaryList...)
//pagination
if ListServices.NextToken == nil {
break
}
PaginationControl = ListServices.NextToken
}
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
return services, nil
}
+25
View File
@@ -0,0 +1,25 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apprunner"
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
)
type MockedAppRunnerClient struct {
}
func (m *MockedAppRunnerClient) ListServices(ctx context.Context, input *apprunner.ListServicesInput, options ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error) {
return &apprunner.ListServicesOutput{
ServiceSummaryList: []apprunnerTypes.ServiceSummary{
{
ServiceName: aws.String("service1"),
},
{
ServiceName: aws.String("service2"),
},
},
}, nil
}
+123
View File
@@ -0,0 +1,123 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/athena"
athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSAthenaClientInterface interface {
ListDataCatalogs(context.Context, *athena.ListDataCatalogsInput, ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error)
ListDatabases(context.Context, *athena.ListDatabasesInput, ...func(*athena.Options)) (*athena.ListDatabasesOutput, error)
}
func init() {
gob.Register([]athenaTypes.DataCatalogSummary{})
}
func CachedAthenaListDataCatalogs(client AWSAthenaClientInterface, accountID string, region string) ([]athenaTypes.DataCatalogSummary, error) {
var PaginationControl *string
var dataCatalogs []athenaTypes.DataCatalogSummary
cacheKey := fmt.Sprintf("%s-athena-ListDataCatalogs-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDataCatalogs",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]athenaTypes.DataCatalogSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDataCatalogs",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDataCatalogs, err := client.ListDataCatalogs(
context.TODO(),
&athena.ListDataCatalogsInput{
NextToken: PaginationControl,
},
func(o *athena.Options) {
o.Region = region
},
)
if err != nil {
return dataCatalogs, err
}
dataCatalogs = append(dataCatalogs, ListDataCatalogs.DataCatalogsSummary...)
//pagination
if ListDataCatalogs.NextToken == nil {
break
}
PaginationControl = ListDataCatalogs.NextToken
}
internal.Cache.Set(cacheKey, dataCatalogs, cache.DefaultExpiration)
return dataCatalogs, nil
}
func CachedAthenaListDatabases(client AWSAthenaClientInterface, accountID string, region string, catalogName string) ([]string, error) {
var PaginationControl *string
var databases []string
cacheKey := fmt.Sprintf("%s-athena-ListDatabases-%s-%s", accountID, region, catalogName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDatabases",
"account": accountID,
"region": region,
"catalogName": catalogName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDatabases",
"account": accountID,
"region": region,
"catalogName": catalogName,
"cache": "miss",
}).Info("AWS API call")
for {
ListDatabases, err := client.ListDatabases(
context.TODO(),
&athena.ListDatabasesInput{
CatalogName: &catalogName,
NextToken: PaginationControl,
},
func(o *athena.Options) {
o.Region = region
},
)
if err != nil {
return databases, err
}
for _, database := range ListDatabases.DatabaseList {
databases = append(databases, *database.Name)
}
//pagination
if ListDatabases.NextToken == nil {
break
}
PaginationControl = ListDatabases.NextToken
}
internal.Cache.Set(cacheKey, databases, cache.DefaultExpiration)
return databases, nil
}
+38
View File
@@ -0,0 +1,38 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/athena"
athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types"
)
type MockedAWSAthenaClient struct {
}
func (m *MockedAWSAthenaClient) ListDatabases(ctx context.Context, input *athena.ListDatabasesInput, options ...func(*athena.Options)) (*athena.ListDatabasesOutput, error) {
return &athena.ListDatabasesOutput{
DatabaseList: []athenaTypes.Database{
{
Name: aws.String("db1"),
},
{
Name: aws.String("db2"),
},
},
}, nil
}
func (m *MockedAWSAthenaClient) ListDataCatalogs(ctx context.Context, input *athena.ListDataCatalogsInput, options ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error) {
return &athena.ListDataCatalogsOutput{
DataCatalogsSummary: []athenaTypes.DataCatalogSummary{
{
CatalogName: aws.String("catalog1"),
},
{
CatalogName: aws.String("catalog2"),
},
},
}, nil
}
+111
View File
@@ -0,0 +1,111 @@
package sdk
import (
"context"
"encoding/gob"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/cloud9"
cloud9Types "github.com/aws/aws-sdk-go-v2/service/cloud9/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCloud9ClientInterface interface {
ListEnvironments(context.Context, *cloud9.ListEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error)
DescribeEnvironments(context.Context, *cloud9.DescribeEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error)
}
func init() {
gob.Register([]string{})
}
func CachedCloud9ListEnvironments(client AWSCloud9ClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var environments []string
cacheKey := "cloud9-ListEnvironments-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:ListEnvironments",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:ListEnvironments",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListEnvironments, err := client.ListEnvironments(
context.TODO(),
&cloud9.ListEnvironmentsInput{
NextToken: PaginationControl,
},
func(o *cloud9.Options) {
o.Region = region
},
)
if err != nil {
return environments, err
}
environments = append(environments, ListEnvironments.EnvironmentIds...)
//pagination
if ListEnvironments.NextToken == nil {
break
}
PaginationControl = ListEnvironments.NextToken
}
internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration)
return environments, nil
}
func CachedCloud9DescribeEnvironments(client AWSCloud9ClientInterface, accountID string, region string, environmentIDs []string) ([]cloud9Types.Environment, error) {
var environments []cloud9Types.Environment
cacheKey := "cloud9-DescribeEnvironments-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:DescribeEnvironments",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloud9Types.Environment), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:DescribeEnvironments",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for _, environmentID := range environmentIDs {
DescribeEnvironments, err := client.DescribeEnvironments(
context.TODO(),
&cloud9.DescribeEnvironmentsInput{
EnvironmentIds: []string{environmentID},
},
func(o *cloud9.Options) {
o.Region = region
},
)
if err != nil {
return environments, err
}
environments = append(environments, DescribeEnvironments.Environments...)
}
internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration)
return environments, nil
}
+36
View File
@@ -0,0 +1,36 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloud9"
"github.com/aws/aws-sdk-go-v2/service/cloud9/types"
)
type MockedAWSCloud9Client struct {
}
func (m *MockedAWSCloud9Client) ListEnvironments(ctx context.Context, input *cloud9.ListEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error) {
return &cloud9.ListEnvironmentsOutput{
EnvironmentIds: []string{
"env1",
"env2",
},
}, nil
}
func (m *MockedAWSCloud9Client) DescribeEnvironments(ctx context.Context, input *cloud9.DescribeEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error) {
return &cloud9.DescribeEnvironmentsOutput{
Environments: []types.Environment{
{
Name: aws.String("env1"),
Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env1"),
},
{
Name: aws.String("env2"),
Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env2"),
},
},
}, nil
}
+156
View File
@@ -0,0 +1,156 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
cloudFormationTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type CloudFormationClientInterface interface {
GetTemplate(context.Context, *cloudformation.GetTemplateInput, ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error)
DescribeStacks(context.Context, *cloudformation.DescribeStacksInput, ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error)
ListStacks(context.Context, *cloudformation.ListStacksInput, ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error)
}
func init() {
gob.Register([]cloudFormationTypes.Stack{})
gob.Register([]cloudFormationTypes.StackSummary{})
}
func CachedCloudFormationDescribeStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.Stack, error) {
var PaginationControl *string
var stacks []cloudFormationTypes.Stack
cacheKey := fmt.Sprintf("%s-cloudformation-DescribeStacks-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:DescribeStacks",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloudFormationTypes.Stack), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:DescribeStacks",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeStacks, err := client.DescribeStacks(
context.TODO(),
&cloudformation.DescribeStacksInput{
NextToken: PaginationControl,
},
func(o *cloudformation.Options) {
o.Region = region
},
)
if err != nil {
return stacks, err
}
stacks = append(stacks, DescribeStacks.Stacks...)
//pagination
if DescribeStacks.NextToken == nil {
break
}
PaginationControl = DescribeStacks.NextToken
}
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
return stacks, nil
}
func CachedCloudFormationGetTemplate(client CloudFormationClientInterface, accountID string, region string, stackName string) (string, error) {
var stackTemplateBody string
cacheKey := fmt.Sprintf("%s-cloudformation-GetTemplate-%s-%s", accountID, region, stackName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:GetTemplate",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:GetTemplate",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
GetTemplate, err := client.GetTemplate(
context.TODO(),
&cloudformation.GetTemplateInput{
StackName: &stackName,
}, func(o *cloudformation.Options) {
o.Region = region
},
)
if err != nil {
return stackTemplateBody, err
}
stackTemplateBody = aws.ToString(GetTemplate.TemplateBody)
internal.Cache.Set(cacheKey, *GetTemplate.TemplateBody, cache.DefaultExpiration)
return stackTemplateBody, nil
}
func CachedCloudFormationListStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.StackSummary, error) {
var PaginationControl *string
var stacks []cloudFormationTypes.StackSummary
cacheKey := fmt.Sprintf("%s-cloudformation-ListStacks-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:ListStacks",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloudFormationTypes.StackSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:ListStacks",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListStacks, err := client.ListStacks(
context.TODO(),
&cloudformation.ListStacksInput{
NextToken: PaginationControl,
},
func(o *cloudformation.Options) {
o.Region = region
},
)
if err != nil {
return stacks, err
}
stacks = append(stacks, ListStacks.StackSummaries...)
//pagination
if ListStacks.NextToken == nil {
break
}
PaginationControl = ListStacks.NextToken
}
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
return stacks, nil
}
+73
View File
@@ -0,0 +1,73 @@
package sdk
import (
"context"
"encoding/json"
"log"
"time"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
)
const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json"
const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json"
type Stacks struct {
Stacks []struct {
StackID string `json:"StackId"`
Description string `json:"Description"`
Tags []interface{} `json:"Tags"`
Outputs []types.Output `json:"Outputs"`
Parameters []types.Parameter `json:"Parameters"`
StackStatusReason interface{} `json:"StackStatusReason"`
CreationTime time.Time `json:"CreationTime"`
Capabilities []interface{} `json:"Capabilities"`
StackName string `json:"StackName"`
RoleArn string `json:"RoleArn"`
StackStatus string `json:"StackStatus"`
DisableRollback bool `json:"DisableRollback"`
} `json:"Stacks"`
}
type TemplateBody struct {
TemplateBody string `json:"TemplateBody"`
}
type MockedCloudformationClient struct {
describeStacks Stacks
getTemplateBody TemplateBody
}
func (m *MockedCloudformationClient) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE)
}
var stacks []types.Stack
for _, stack := range m.describeStacks.Stacks {
stacks = append(stacks, types.Stack{
StackName: &stack.StackName,
RoleARN: &stack.RoleArn,
Outputs: stack.Outputs,
Parameters: stack.Parameters,
})
}
return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil
}
func (m *MockedCloudformationClient) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody)
if err != nil {
log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE)
}
return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil
}
func (m *MockedCloudformationClient) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) {
return &cloudformation.ListStacksOutput{}, nil
}
+67
View File
@@ -0,0 +1,67 @@
package sdk
import (
"context"
"encoding/gob"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCloudFrontClientInterface interface {
ListDistributions(ctx context.Context, params *cloudfront.ListDistributionsInput, optFns ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error)
}
func init() {
gob.Register([]cloudfrontTypes.DistributionSummary{})
gob.Register(cloudfrontTypes.DistributionSummary{})
}
func CachedCloudFrontListDistributions(CloudFrontClient AWSCloudFrontClientInterface, accountID string) ([]cloudfrontTypes.DistributionSummary, error) {
var PaginationControl *string
var distributions []cloudfrontTypes.DistributionSummary
cacheKey := "cloudfront-ListDistributions-" + accountID
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloudfront:ListDistributions",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloudfrontTypes.DistributionSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudfront:ListDistributions",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
ListDistributions, err := CloudFrontClient.ListDistributions(
context.TODO(),
&cloudfront.ListDistributionsInput{
Marker: PaginationControl,
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
distributions = append(distributions, ListDistributions.DistributionList.Items...)
// Pagination control.
if ListDistributions.DistributionList.NextMarker != nil {
PaginationControl = ListDistributions.DistributionList.NextMarker
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, distributions, cache.DefaultExpiration)
return distributions, nil
}
+27
View File
@@ -0,0 +1,27 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
)
type MockedAWSCloudFrontClient struct {
}
func (m *MockedAWSCloudFrontClient) ListDistributions(ctx context.Context, input *cloudfront.ListDistributionsInput, options ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error) {
return &cloudfront.ListDistributionsOutput{
DistributionList: &cloudfrontTypes.DistributionList{
Items: []cloudfrontTypes.DistributionSummary{
{
Id: aws.String("distribution1"),
},
{
Id: aws.String("distribution2"),
},
},
},
}, nil
}
+119
View File
@@ -0,0 +1,119 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/codeartifact"
codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCodeArtifactClientInterface interface {
ListDomains(context.Context, *codeartifact.ListDomainsInput, ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error)
ListRepositories(context.Context, *codeartifact.ListRepositoriesInput, ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error)
}
func init() {
gob.Register([]codeArtifactTypes.DomainSummary{})
gob.Register([]codeArtifactTypes.RepositorySummary{})
}
func CachedCodeArtifactListDomains(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.DomainSummary, error) {
var PaginationControl *string
var domains []codeArtifactTypes.DomainSummary
cacheKey := fmt.Sprintf("%s-codeartifact-ListDomains-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListDomains",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]codeArtifactTypes.DomainSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListDomains",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDomains, err := client.ListDomains(
context.TODO(),
&codeartifact.ListDomainsInput{
NextToken: PaginationControl,
},
func(o *codeartifact.Options) {
o.Region = region
},
)
if err != nil {
return domains, err
}
domains = append(domains, ListDomains.Domains...)
//pagination
if ListDomains.NextToken == nil {
break
}
PaginationControl = ListDomains.NextToken
}
internal.Cache.Set(cacheKey, domains, cache.DefaultExpiration)
return domains, nil
}
func CachedCodeArtifactListRepositories(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.RepositorySummary, error) {
var PaginationControl *string
var repositories []codeArtifactTypes.RepositorySummary
cacheKey := fmt.Sprintf("%s-codeartifact-ListRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListRepositories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]codeArtifactTypes.RepositorySummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListRepositories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListRepositories, err := client.ListRepositories(
context.TODO(),
&codeartifact.ListRepositoriesInput{
NextToken: PaginationControl,
},
func(o *codeartifact.Options) {
o.Region = region
},
)
if err != nil {
return repositories, err
}
repositories = append(repositories, ListRepositories.Repositories...)
//pagination
if ListRepositories.NextToken == nil {
break
}
PaginationControl = ListRepositories.NextToken
}
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
return repositories, nil
}
+46
View File
@@ -0,0 +1,46 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codeartifact"
codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types"
)
type MockedAWSCodeArtifactClient struct {
}
func (m *MockedAWSCodeArtifactClient) ListRepositories(ctx context.Context, input *codeartifact.ListRepositoriesInput, options ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error) {
return &codeartifact.ListRepositoriesOutput{
NextToken: nil,
Repositories: []codeArtifactTypes.RepositorySummary{
{
Name: aws.String("repo1"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo1"),
DomainName: aws.String("domain1"),
},
{
Name: aws.String("repo2"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo2"),
DomainName: aws.String("domain1"),
},
},
}, nil
}
func (m *MockedAWSCodeArtifactClient) ListDomains(ctx context.Context, input *codeartifact.ListDomainsInput, options ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error) {
return &codeartifact.ListDomainsOutput{
NextToken: nil,
Domains: []codeArtifactTypes.DomainSummary{
{
Name: aws.String("domain1"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain1"),
},
{
Name: aws.String("domain2"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain2"),
},
},
}, nil
}
+156
View File
@@ -0,0 +1,156 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codebuild"
codeBuildTypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type CodeBuildClientInterface interface {
ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error)
BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error)
GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error)
}
func init() {
gob.Register(codeBuildTypes.Project{})
gob.Register([]codeBuildTypes.Project{})
}
// create a CachedCodeBuildListProjects function that accepts a codebuild client, account id, and region. Make sure it handles the region option and pagination
func CachedCodeBuildListProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var projects []string
cacheKey := fmt.Sprintf("%s-codebuild-ListProjects", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:ListProjects",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:ListProjects",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListProjects, err := CodeBuildClient.ListProjects(
context.TODO(),
&codebuild.ListProjectsInput{
NextToken: PaginationControl,
},
func(o *codebuild.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
projects = append(projects, ListProjects.Projects...)
// Pagination control.
if ListProjects.NextToken != nil {
PaginationControl = ListProjects.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, projects, cache.DefaultExpiration)
return projects, nil
}
// create a CachedCodeBuildBatchGetProjects function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (codeBuildTypes.Project, error) {
project := codeBuildTypes.Project{}
cacheKey := fmt.Sprintf("%s-codebuild-BatchGetProjects-%s-%s", accountID, region, projectID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:BatchGetProjects",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeBuildTypes.Project), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:BatchGetProjects",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
BatchGetProjects, err := CodeBuildClient.BatchGetProjects(
context.TODO(),
&codebuild.BatchGetProjectsInput{
Names: []string{projectID},
},
func(o *codebuild.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return project, err
}
internal.Cache.Set(cacheKey, BatchGetProjects.Projects, cache.DefaultExpiration)
return BatchGetProjects.Projects[0], nil
}
// create a CachedCodeBuildGetResourcePolicy function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
func CachedCodeBuildGetResourcePolicy(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (string, error) {
cacheKey := fmt.Sprintf("%s-codebuild-GetResourcePolicy-%s-%s", accountID, region, projectID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:GetResourcePolicy",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:GetResourcePolicy",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
GetResourcePolicy, err := CodeBuildClient.GetResourcePolicy(
context.TODO(),
&codebuild.GetResourcePolicyInput{
ResourceArn: aws.String("arn:aws:codebuild:" + region + ":" + accountID + ":project/" + projectID),
},
func(o *codebuild.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return "", err
}
internal.Cache.Set(cacheKey, GetResourcePolicy, cache.DefaultExpiration)
return aws.ToString(GetResourcePolicy.Policy), nil
}
+53
View File
@@ -0,0 +1,53 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codebuild"
codeBuildTypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types"
)
type MockedCodeBuildClient struct {
}
func (m *MockedCodeBuildClient) ListProjects(ctx context.Context, input *codebuild.ListProjectsInput, options ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) {
return &codebuild.ListProjectsOutput{
Projects: []string{
"project1",
"project2",
},
}, nil
}
func (m *MockedCodeBuildClient) BatchGetProjects(ctx context.Context, input *codebuild.BatchGetProjectsInput, options ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error) {
return &codebuild.BatchGetProjectsOutput{
Projects: []codeBuildTypes.Project{
{
Name: aws.String("project1"),
},
{
Name: aws.String("project2"),
},
},
}, nil
}
func (m *MockedCodeBuildClient) GetResourcePolicy(ctx context.Context, input *codebuild.GetResourcePolicyInput, options ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) {
return &codebuild.GetResourcePolicyOutput{
Policy: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "codebuild:BatchGetProjects",
"Resource": "*",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
}
]
}
`),
}, nil
}
+69
View File
@@ -0,0 +1,69 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/codecommit"
codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCodeCommitClientInterface interface {
ListRepositories(context.Context, *codecommit.ListRepositoriesInput, ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error)
}
func init() {
gob.Register([]codeCommitTypes.RepositoryNameIdPair{})
}
func CachedCodeCommitListRepositories(client AWSCodeCommitClientInterface, accountID string, region string) ([]codeCommitTypes.RepositoryNameIdPair, error) {
var PaginationControl *string
var repositories []codeCommitTypes.RepositoryNameIdPair
cacheKey := fmt.Sprintf("%s-codecommit-ListRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codecommit:ListRepositories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]codeCommitTypes.RepositoryNameIdPair), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codecommit:ListRepositories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListRepositories, err := client.ListRepositories(
context.TODO(),
&codecommit.ListRepositoriesInput{
NextToken: PaginationControl,
},
func(o *codecommit.Options) {
o.Region = region
},
)
if err != nil {
return repositories, err
}
repositories = append(repositories, ListRepositories.Repositories...)
//pagination
if ListRepositories.NextToken == nil {
break
}
PaginationControl = ListRepositories.NextToken
}
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
return repositories, nil
}
+30
View File
@@ -0,0 +1,30 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codecommit"
codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types"
)
// create mocks for codecommit ListRepositories
type MockedAWSCodeCommitClient struct {
}
func (m *MockedAWSCodeCommitClient) ListRepositories(ctx context.Context, input *codecommit.ListRepositoriesInput, options ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error) {
return &codecommit.ListRepositoriesOutput{
NextToken: nil,
Repositories: []codeCommitTypes.RepositoryNameIdPair{
{
RepositoryId: aws.String("repo1"),
RepositoryName: aws.String("repo1"),
},
{
RepositoryId: aws.String("repo2"),
RepositoryName: aws.String("repo2"),
},
},
}, nil
}
+291
View File
@@ -0,0 +1,291 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/codedeploy"
codeDeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCodeDeployClientInterface interface {
ListApplications(context.Context, *codedeploy.ListApplicationsInput, ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error)
GetApplication(context.Context, *codedeploy.GetApplicationInput, ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error)
ListDeployments(context.Context, *codedeploy.ListDeploymentsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error)
GetDeployment(context.Context, *codedeploy.GetDeploymentInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error)
ListDeploymentConfigs(context.Context, *codedeploy.ListDeploymentConfigsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error)
GetDeploymentConfig(context.Context, *codedeploy.GetDeploymentConfigInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error)
}
func init() {
gob.Register([]string{})
gob.Register([]codeDeployTypes.ApplicationInfo{})
gob.Register([]codeDeployTypes.DeploymentConfigInfo{})
gob.Register([]codeDeployTypes.DeploymentInfo{})
}
func CachedCodeDeployListApplications(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var applications []string
cacheKey := fmt.Sprintf("%s-codedeploy-ListApplications-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListApplications",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListApplications",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListApplications, err := client.ListApplications(
context.TODO(),
&codedeploy.ListApplicationsInput{
NextToken: PaginationControl,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return applications, err
}
applications = append(applications, ListApplications.Applications...)
//pagination
if ListApplications.NextToken == nil {
break
}
PaginationControl = ListApplications.NextToken
}
internal.Cache.Set(cacheKey, applications, cache.DefaultExpiration)
return applications, nil
}
func CachedCodeDeployListDeploymentConfigs(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var deploymentConfigs []string
cacheKey := fmt.Sprintf("%s-codedeploy-ListDeploymentConfigs-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeploymentConfigs",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeploymentConfigs",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDeploymentConfigs, err := client.ListDeploymentConfigs(
context.TODO(),
&codedeploy.ListDeploymentConfigsInput{
NextToken: PaginationControl,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return deploymentConfigs, err
}
deploymentConfigs = append(deploymentConfigs, ListDeploymentConfigs.DeploymentConfigsList...)
//pagination
if ListDeploymentConfigs.NextToken == nil {
break
}
PaginationControl = ListDeploymentConfigs.NextToken
}
internal.Cache.Set(cacheKey, deploymentConfigs, cache.DefaultExpiration)
return deploymentConfigs, nil
}
func CachedCodeDeployListDeployments(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var deployments []string
cacheKey := fmt.Sprintf("%s-codedeploy-ListDeployments-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeployments",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeployments",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDeployments, err := client.ListDeployments(
context.TODO(),
&codedeploy.ListDeploymentsInput{
NextToken: PaginationControl,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return deployments, err
}
deployments = append(deployments, ListDeployments.Deployments...)
//pagination
if ListDeployments.NextToken == nil {
break
}
PaginationControl = ListDeployments.NextToken
}
internal.Cache.Set(cacheKey, deployments, cache.DefaultExpiration)
return deployments, nil
}
func CachedCodeDeployGetApplication(client AWSCodeDeployClientInterface, accountID string, region string, applicationName string) (codeDeployTypes.ApplicationInfo, error) {
cacheKey := fmt.Sprintf("%s-codedeploy-GetApplication-%s-%s", accountID, region, applicationName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetApplication",
"account": accountID,
"region": region,
"application": applicationName,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeDeployTypes.ApplicationInfo), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetApplication",
"account": accountID,
"region": region,
"application": applicationName,
"cache": "miss",
}).Info("AWS API call")
GetApplication, err := client.GetApplication(
context.TODO(),
&codedeploy.GetApplicationInput{
ApplicationName: &applicationName,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return *GetApplication.Application, err
}
internal.Cache.Set(cacheKey, *GetApplication.Application, cache.DefaultExpiration)
return *GetApplication.Application, nil
}
func CachedCodeDeployGetDeploymentConfig(client AWSCodeDeployClientInterface, accountID string, region string, deploymentConfigName string) (codeDeployTypes.DeploymentConfigInfo, error) {
cacheKey := fmt.Sprintf("%s-codedeploy-GetDeploymentConfig-%s-%s", accountID, region, deploymentConfigName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeploymentConfig",
"account": accountID,
"region": region,
"deploymentConfig": deploymentConfigName,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeDeployTypes.DeploymentConfigInfo), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeploymentConfig",
"account": accountID,
"region": region,
"deploymentConfig": deploymentConfigName,
"cache": "miss",
}).Info("AWS API call")
GetDeploymentConfig, err := client.GetDeploymentConfig(
context.TODO(),
&codedeploy.GetDeploymentConfigInput{
DeploymentConfigName: &deploymentConfigName,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return *GetDeploymentConfig.DeploymentConfigInfo, err
}
internal.Cache.Set(cacheKey, *GetDeploymentConfig.DeploymentConfigInfo, cache.DefaultExpiration)
return *GetDeploymentConfig.DeploymentConfigInfo, nil
}
func CachedCodeDeployGetDeployment(client AWSCodeDeployClientInterface, accountID string, region string, deploymentID string) (codeDeployTypes.DeploymentInfo, error) {
cacheKey := fmt.Sprintf("%s-codedeploy-GetDeployment-%s-%s", accountID, region, deploymentID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeployment",
"account": accountID,
"region": region,
"deployment": deploymentID,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeDeployTypes.DeploymentInfo), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeployment",
"account": accountID,
"region": region,
"deployment": deploymentID,
"cache": "miss",
}).Info("AWS API call")
GetDeployment, err := client.GetDeployment(
context.TODO(),
&codedeploy.GetDeploymentInput{
DeploymentId: &deploymentID,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return *GetDeployment.DeploymentInfo, err
}
internal.Cache.Set(cacheKey, *GetDeployment.DeploymentInfo, cache.DefaultExpiration)
return *GetDeployment.DeploymentInfo, nil
}
+85
View File
@@ -0,0 +1,85 @@
package sdk
import (
"context"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codedeploy"
codedeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types"
)
type MockedCodedeployClient struct {
}
func (m *MockedCodedeployClient) ListApplications(ctx context.Context, input *codedeploy.ListApplicationsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error) {
return &codedeploy.ListApplicationsOutput{
Applications: []string{
"app1",
"app2",
},
}, nil
}
func (m *MockedCodedeployClient) ListDeployments(ctx context.Context, input *codedeploy.ListDeploymentsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error) {
return &codedeploy.ListDeploymentsOutput{
Deployments: []string{
"deployment1",
"deployment2",
},
}, nil
}
func (m *MockedCodedeployClient) ListDeploymentConfigs(ctx context.Context, input *codedeploy.ListDeploymentConfigsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error) {
return &codedeploy.ListDeploymentConfigsOutput{
DeploymentConfigsList: []string{
"deploymentConfig1",
"deploymentConfig2",
},
}, nil
}
func (m *MockedCodedeployClient) GetApplication(ctx context.Context, input *codedeploy.GetApplicationInput, options ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error) {
return &codedeploy.GetApplicationOutput{
Application: &codedeployTypes.ApplicationInfo{
ApplicationName: aws.String("application1"),
ApplicationId: aws.String("application1"),
CreateTime: aws.Time(time.Now()),
GitHubAccountName: aws.String("github"),
LinkedToGitHub: true,
},
}, nil
}
func (m *MockedCodedeployClient) GetDeployment(ctx context.Context, input *codedeploy.GetDeploymentInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error) {
return &codedeploy.GetDeploymentOutput{
DeploymentInfo: &codedeployTypes.DeploymentInfo{
ApplicationName: aws.String("application1"),
CreateTime: aws.Time(time.Now()),
DeploymentId: aws.String("deployment1"),
DeploymentOverview: &codedeployTypes.DeploymentOverview{
Failed: 0,
InProgress: 0,
Pending: 0,
Ready: 0,
Skipped: 0,
Succeeded: 0,
},
DeploymentConfigName: aws.String("deploymentConfig1"),
Status: codedeployTypes.DeploymentStatusSucceeded,
},
}, nil
}
func (m *MockedCodedeployClient) GetDeploymentConfig(ctx context.Context, input *codedeploy.GetDeploymentConfigInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error) {
return &codedeploy.GetDeploymentConfigOutput{
DeploymentConfigInfo: &codedeployTypes.DeploymentConfigInfo{
DeploymentConfigName: aws.String("deploymentConfig1"),
ComputePlatform: codedeployTypes.ComputePlatformServer,
MinimumHealthyHosts: &codedeployTypes.MinimumHealthyHosts{
Type: codedeployTypes.MinimumHealthyHostsTypeFleetPercent,
Value: 100,
},
},
}, nil
}
+69
View File
@@ -0,0 +1,69 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/datapipeline"
dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSDataPipelineClientInterface interface {
ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, opts ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error)
}
func init() {
gob.Register([]dataPipelineTypes.PipelineIdName{})
}
func CachedDataPipelineListPipelines(client AWSDataPipelineClientInterface, accountID string, region string) ([]dataPipelineTypes.PipelineIdName, error) {
var PaginationControl *string
var pipelines []dataPipelineTypes.PipelineIdName
cacheKey := fmt.Sprintf("%s-datapipeline-ListPipelines-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "datapipeline:ListPipelines",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]dataPipelineTypes.PipelineIdName), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "datapipeline:ListPipelines",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListPipelines, err := client.ListPipelines(
context.TODO(),
&datapipeline.ListPipelinesInput{
Marker: PaginationControl,
},
func(o *datapipeline.Options) {
o.Region = region
},
)
if err != nil {
return pipelines, err
}
pipelines = append(pipelines, ListPipelines.PipelineIdList...)
//pagination
if ListPipelines.Marker == nil {
break
}
PaginationControl = ListPipelines.Marker
}
internal.Cache.Set(cacheKey, pipelines, cache.DefaultExpiration)
return pipelines, nil
}
+27
View File
@@ -0,0 +1,27 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/datapipeline"
dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types"
)
type MockedDataPipelineClient struct {
}
func (m *MockedDataPipelineClient) ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, options ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error) {
return &datapipeline.ListPipelinesOutput{
PipelineIdList: []dataPipelineTypes.PipelineIdName{
{
Id: aws.String("pipeline1"),
Name: aws.String("pipeline1"),
},
{
Id: aws.String("pipeline2"),
Name: aws.String("pipeline2"),
},
},
}, nil
}
+177
View File
@@ -0,0 +1,177 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/docdb"
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type DocDBClientInterface interface {
DescribeGlobalClusters(context.Context, *docdb.DescribeGlobalClustersInput, ...func(*docdb.Options)) (*docdb.DescribeGlobalClustersOutput, error)
DescribeDBClusters(context.Context, *docdb.DescribeDBClustersInput, ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error)
DescribeDBInstances(context.Context, *docdb.DescribeDBInstancesInput, ...func(*docdb.Options)) (*docdb.DescribeDBInstancesOutput, error)
}
func init() {
gob.Register([]docdbTypes.GlobalCluster{})
//gob.Register([]docdbTypes.DBCluster{})
//gob.Register([]docdbTypes.DBInstance{})
}
func CachedDocDBDescribeGlobalClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.GlobalCluster, error) {
var PaginationControl *string
var globalClusters []docdbTypes.GlobalCluster
cacheKey := fmt.Sprintf("%s-docdb-DescribeGlobalClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeGlobalClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]docdbTypes.GlobalCluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeGlobalClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeGlobalClusters, err := client.DescribeGlobalClusters(
context.TODO(),
&docdb.DescribeGlobalClustersInput{
Marker: PaginationControl,
},
func(o *docdb.Options) {
o.Region = region
},
)
if err != nil {
return globalClusters, err
}
globalClusters = append(globalClusters, DescribeGlobalClusters.GlobalClusters...)
//pagination
if DescribeGlobalClusters.Marker == nil {
break
}
PaginationControl = DescribeGlobalClusters.Marker
}
internal.Cache.Set(cacheKey, globalClusters, cache.DefaultExpiration)
return globalClusters, nil
}
func CachedDocDBDescribeDBClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBCluster, error) {
var PaginationControl *string
var dbClusters []docdbTypes.DBCluster
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]docdbTypes.DBCluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDBClusters, err := client.DescribeDBClusters(
context.TODO(),
&docdb.DescribeDBClustersInput{
Marker: PaginationControl,
Filters: []docdbTypes.Filter{
{
Name: aws.String("engine"),
Values: []string{"docdb"},
},
},
},
func(o *docdb.Options) {
o.Region = region
},
)
if err != nil {
return dbClusters, err
}
dbClusters = append(dbClusters, DescribeDBClusters.DBClusters...)
//pagination
if DescribeDBClusters.Marker == nil {
break
}
PaginationControl = DescribeDBClusters.Marker
}
internal.Cache.Set(cacheKey, dbClusters, cache.DefaultExpiration)
return dbClusters, nil
}
func CachedDocDBDescribeDBInstances(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBInstance, error) {
var PaginationControl *string
var dbInstances []docdbTypes.DBInstance
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBInstances",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]docdbTypes.DBInstance), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBInstances",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDBInstances, err := client.DescribeDBInstances(
context.TODO(),
&docdb.DescribeDBInstancesInput{
Marker: PaginationControl,
},
func(o *docdb.Options) {
o.Region = region
},
)
if err != nil {
return dbInstances, err
}
dbInstances = append(dbInstances, DescribeDBInstances.DBInstances...)
//pagination
if DescribeDBInstances.Marker == nil {
break
}
PaginationControl = DescribeDBInstances.Marker
}
internal.Cache.Set(cacheKey, dbInstances, cache.DefaultExpiration)
return dbInstances, nil
}
+25
View File
@@ -0,0 +1,25 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/docdb"
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
)
type MockedAWSDocsDBClient struct {
}
func (m *MockedAWSDocsDBClient) DescribeDBClusters(ctx context.Context, input *docdb.DescribeDBClustersInput, options ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error) {
return &docdb.DescribeDBClustersOutput{
DBClusters: []docdbTypes.DBCluster{
{
DBClusterIdentifier: aws.String("cluster1"),
},
{
DBClusterIdentifier: aws.String("cluster2"),
},
},
}, nil
}
+122
View File
@@ -0,0 +1,122 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/patrickmn/go-cache"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/directoryservice"
dsTypes "github.com/aws/aws-sdk-go-v2/service/directoryservice/types"
"github.com/sirupsen/logrus"
)
type AWSDSClientInterface interface {
DescribeDirectories(context.Context, *directoryservice.DescribeDirectoriesInput, ...func(*directoryservice.Options)) (*directoryservice.DescribeDirectoriesOutput, error)
DescribeTrusts(context.Context, *directoryservice.DescribeTrustsInput, ...func(*directoryservice.Options)) (*directoryservice.DescribeTrustsOutput, error)
}
func init() {
gob.Register([]dsTypes.DirectoryDescription{})
gob.Register([]dsTypes.Trust{})
}
func CachedDSDescribeDirectories(client AWSDSClientInterface, accountID string, region string) ([]dsTypes.DirectoryDescription, error) {
var PaginationControl *string
var directories []dsTypes.DirectoryDescription
cacheKey := fmt.Sprintf("%s-ds-DescribeDirectories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeDirectories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]dsTypes.DirectoryDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeDirectories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDirectories, err := client.DescribeDirectories(
context.TODO(),
&directoryservice.DescribeDirectoriesInput{
NextToken: PaginationControl,
},
func(o *directoryservice.Options) {
o.Region = region
},
)
if err != nil {
return directories, err
}
directories = append(directories, DescribeDirectories.DirectoryDescriptions...)
//pagination
if DescribeDirectories.NextToken == nil {
break
}
PaginationControl = DescribeDirectories.NextToken
}
internal.Cache.Set(cacheKey, directories, cache.DefaultExpiration)
return directories, nil
}
func CachedDSDescribeTrusts(client AWSDSClientInterface, accountID string, region string, directoryId string) ([]dsTypes.Trust, error) {
var PaginationControl *string
var trusts []dsTypes.Trust
cacheKey := fmt.Sprintf("%s-ds-DescribeTrusts-%s-%s", accountID, region, directoryId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeTrusts",
"account": accountID,
"region": region,
"directoryId": directoryId,
"cache": "hit",
}).Info("AWS API call")
return cached.([]dsTypes.Trust), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeTrusts",
"account": accountID,
"region": region,
"directoryId": directoryId,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDirectoryTrusts, err := client.DescribeTrusts(
context.TODO(),
&directoryservice.DescribeTrustsInput{
DirectoryId: &directoryId,
NextToken: PaginationControl,
},
func(o *directoryservice.Options) {
o.Region = region
},
)
if err != nil {
return trusts, err
}
trusts = append(trusts, DescribeDirectoryTrusts.Trusts...)
//pagination
if DescribeDirectoryTrusts.NextToken == nil {
break
}
PaginationControl = DescribeDirectoryTrusts.NextToken
}
internal.Cache.Set(cacheKey, trusts, cache.DefaultExpiration)
return trusts, nil
}
+112
View File
@@ -0,0 +1,112 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dynamoDBTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type DynamoDBClientInterface interface {
ListTables(context.Context, *dynamodb.ListTablesInput, ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error)
DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error)
}
func init() {
gob.Register([]string{})
gob.Register(dynamoDBTypes.TableDescription{})
}
func CachedDynamoDBListTables(client DynamoDBClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var tables []string
cacheKey := fmt.Sprintf("%s-dynamodb-ListTables-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:ListTables",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:ListTables",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListTables, err := client.ListTables(
context.TODO(),
&dynamodb.ListTablesInput{
ExclusiveStartTableName: PaginationControl,
},
func(o *dynamodb.Options) {
o.Region = region
},
)
if err != nil {
return tables, err
}
tables = append(tables, ListTables.TableNames...)
//pagination
if ListTables.LastEvaluatedTableName == nil {
break
}
PaginationControl = ListTables.LastEvaluatedTableName
}
internal.Cache.Set(cacheKey, tables, cache.DefaultExpiration)
return tables, nil
}
func CachedDynamoDBDescribeTable(client DynamoDBClientInterface, accountID string, region string, tableName string) (dynamoDBTypes.TableDescription, error) {
var tableDescription dynamoDBTypes.TableDescription
cacheKey := fmt.Sprintf("%s-dynamodb-DescribeTable-%s-%s", accountID, region, tableName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:DescribeTable",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(dynamoDBTypes.TableDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:DescribeTable",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
DescribeTable, err := client.DescribeTable(
context.TODO(),
&dynamodb.DescribeTableInput{
TableName: &tableName,
},
func(o *dynamodb.Options) {
o.Region = region
},
)
if err != nil {
return tableDescription, err
}
tableDescription = *DescribeTable.Table
internal.Cache.Set(cacheKey, tableDescription, cache.DefaultExpiration)
return tableDescription, nil
}
+25
View File
@@ -0,0 +1,25 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type MockedAWSDynamoDBClient struct {
}
func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dynamodb.DescribeTableInput, options ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
return &dynamodb.DescribeTableOutput{
Table: &dynamodbTypes.TableDescription{
TableName: input.TableName,
},
}, nil
}
func (m *MockedAWSDynamoDBClient) ListTables(ctx context.Context, input *dynamodb.ListTablesInput, options ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) {
return &dynamodb.ListTablesOutput{
TableNames: []string{"table1", "table2"},
}, nil
}
+347
View File
@@ -0,0 +1,347 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSEC2ClientInterface interface {
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
DescribeNetworkInterfaces(context.Context, *ec2.DescribeNetworkInterfacesInput, ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error)
DescribeSnapshots(context.Context, *ec2.DescribeSnapshotsInput, ...func(*ec2.Options)) (*ec2.DescribeSnapshotsOutput, error)
DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error)
DescribeImages(context.Context, *ec2.DescribeImagesInput, ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error)
DescribeInstanceAttribute(context.Context, *ec2.DescribeInstanceAttributeInput, ...func(*ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error)
DescribeVpcEndpoints(context.Context, *ec2.DescribeVpcEndpointsInput, ...func(options *ec2.Options)) (*ec2.DescribeVpcEndpointsOutput, error)
}
func init() {
gob.Register([]ec2Types.Instance{})
gob.Register([]ec2Types.NetworkInterface{})
gob.Register([]ec2Types.Snapshot{})
gob.Register([]ec2Types.Volume{})
gob.Register([]ec2Types.Image{})
gob.Register([]ec2Types.VpcEndpoint{})
}
func CachedEC2DescribeInstances(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) {
var PaginationControl *string
var instances []ec2Types.Instance
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstances",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Instance), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstances",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeInstances, err := client.DescribeInstances(
context.TODO(),
&ec2.DescribeInstancesInput{
NextToken: PaginationControl,
},
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return instances, err
}
for _, reservation := range DescribeInstances.Reservations {
instances = append(instances, reservation.Instances...)
}
//pagination
if DescribeInstances.NextToken == nil {
break
}
PaginationControl = DescribeInstances.NextToken
}
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
return instances, nil
}
func CachedEC2DescribeInstanceAttributeUserData(client AWSEC2ClientInterface, accountID string, region string, instanceID string) (string, error) {
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstanceAttributeUserData-%s-%s", accountID, region, instanceID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstanceAttribute",
"account": accountID,
"region": region,
"instance": instanceID,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstanceAttribute",
"account": accountID,
"region": region,
"instance": instanceID,
"cache": "miss",
}).Info("AWS API call")
DescribeInstanceAttribute, err := client.DescribeInstanceAttribute(
context.TODO(),
&ec2.DescribeInstanceAttributeInput{
Attribute: ec2Types.InstanceAttributeNameUserData,
InstanceId: &instanceID,
},
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return "", err
}
internal.Cache.Set(cacheKey, aws.ToString(DescribeInstanceAttribute.UserData.Value), cache.DefaultExpiration)
return aws.ToString(DescribeInstanceAttribute.UserData.Value), nil
}
func CachedEC2DescribeNetworkInterfaces(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.NetworkInterface, error) {
var PaginationControl *string
var NetworkInterfaces []ec2Types.NetworkInterface
cacheKey := fmt.Sprintf("%s-ec2-DescribeNetworkInterfaces-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeNetworkInterfaces",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.NetworkInterface), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeNetworkInterfaces",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeNetworkInterfaces, err := client.DescribeNetworkInterfaces(
context.TODO(),
&(ec2.DescribeNetworkInterfacesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return NetworkInterfaces, err
}
NetworkInterfaces = append(NetworkInterfaces, DescribeNetworkInterfaces.NetworkInterfaces...)
if DescribeNetworkInterfaces.NextToken == nil {
break
}
PaginationControl = DescribeNetworkInterfaces.NextToken
}
internal.Cache.Set(cacheKey, NetworkInterfaces, cache.DefaultExpiration)
return NetworkInterfaces, nil
}
func CachedEC2DescribeSnapshots(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Snapshot, error) {
var PaginationControl *string
var Snapshots []ec2Types.Snapshot
cacheKey := fmt.Sprintf("%s-ec2-DescribeSnapshots-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeSnapshots",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Snapshot), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeSnapshots",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeSnapshots, err := client.DescribeSnapshots(
context.TODO(),
&(ec2.DescribeSnapshotsInput{
NextToken: PaginationControl,
OwnerIds: []string{accountID},
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return Snapshots, err
}
Snapshots = append(Snapshots, DescribeSnapshots.Snapshots...)
if DescribeSnapshots.NextToken == nil {
break
}
PaginationControl = DescribeSnapshots.NextToken
}
internal.Cache.Set(cacheKey, Snapshots, cache.DefaultExpiration)
return Snapshots, nil
}
func CachedEC2DescribeVolumes(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Volume, error) {
var PaginationControl *string
var Volumes []ec2Types.Volume
cacheKey := fmt.Sprintf("%s-ec2-DescribeVolumes-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVolumes",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Volume), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVolumes",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeVolumes, err := client.DescribeVolumes(
context.TODO(),
&(ec2.DescribeVolumesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return Volumes, err
}
Volumes = append(Volumes, DescribeVolumes.Volumes...)
if DescribeVolumes.NextToken == nil {
break
}
PaginationControl = DescribeVolumes.NextToken
}
internal.Cache.Set(cacheKey, Volumes, cache.DefaultExpiration)
return Volumes, nil
}
func CachedEC2DescribeImages(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Image, error) {
var PaginationControl *string
var Images []ec2Types.Image
cacheKey := fmt.Sprintf("%s-ec2-DescribeImages-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeImages",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Image), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeImages",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeImages, err := client.DescribeImages(
context.TODO(),
&(ec2.DescribeImagesInput{
Owners: []string{accountID},
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return Images, err
}
Images = append(Images, DescribeImages.Images...)
if DescribeImages.NextToken == nil {
break
}
PaginationControl = DescribeImages.NextToken
}
internal.Cache.Set(cacheKey, Images, cache.DefaultExpiration)
return Images, nil
}
func CachedEC2DescribeVpcEndpoints(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.VpcEndpoint, error) {
var PaginationControl *string
var VpcEndpoints []ec2Types.VpcEndpoint
cacheKey := fmt.Sprintf("%s-ec2-DescribeVpcEndpoints-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVpcEndpoints",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.VpcEndpoint), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVpcEndpoints",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeVpcEndpoints, err := client.DescribeVpcEndpoints(
context.TODO(),
&(ec2.DescribeVpcEndpointsInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return VpcEndpoints, err
}
VpcEndpoints = append(VpcEndpoints, DescribeVpcEndpoints.VpcEndpoints...)
if DescribeVpcEndpoints.NextToken == nil {
break
}
PaginationControl = DescribeVpcEndpoints.NextToken
}
internal.Cache.Set(cacheKey, VpcEndpoints, cache.DefaultExpiration)
return VpcEndpoints, nil
}
+432
View File
@@ -0,0 +1,432 @@
package sdk
// create test for the Cached EC2 functions. Create a mocked client and mocked functions for each of the methods used in the ec2.go file
import (
"context"
"encoding/json"
"log"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json"
const DESCRIBE_INSTANCES_TEST_FILE = "./test-data/ec2-describeInstances.json"
const DESCRIBE_SNAPSHOTS_TEST_FILE = "./test-data/ec2-describeSnapshots.json"
const DESCRIBE_VOLUMES_TEST_FILE = "./test-data/ec2-describeVolumes.json"
const DESCRIBE_IMAGES_TEST_FILE = "./test-data/ec2-describeImages.json"
type MockedEC2Client2 struct {
describeNetworkInterfaces DescribeNetworkInterfaces
describeInstances DescribeInstances
describeSnapshots DescribeSnapshots
describeVolumes DescribeVolumes
describeImages DescribeImages
describeVpcEndpoints DescribeVpcEndpoints
}
type DescribeNetworkInterfaces struct {
NetworkInterfaces []struct {
Status string `json:"Status"`
MacAddress string `json:"MacAddress"`
SourceDestCheck bool `json:"SourceDestCheck"`
VpcID string `json:"VpcId"`
Description string `json:"Description"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
NetworkInterfaceID string `json:"NetworkInterfaceId"`
PrivateIPAddresses []struct {
PrivateDNSName string `json:"PrivateDnsName"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
Primary bool `json:"Primary"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"PrivateIpAddresses"`
RequesterManaged bool `json:"RequesterManaged"`
Ipv6Addresses []interface{} `json:"Ipv6Addresses"`
PrivateDNSName string `json:"PrivateDnsName,omitempty"`
AvailabilityZone string `json:"AvailabilityZone"`
Attachment struct {
Status string `json:"Status"`
DeviceIndex int `json:"DeviceIndex"`
AttachTime time.Time `json:"AttachTime"`
InstanceID string `json:"InstanceId"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
AttachmentID string `json:"AttachmentId"`
InstanceOwnerID string `json:"InstanceOwnerId"`
} `json:"Attachment"`
Groups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"Groups"`
SubnetID string `json:"SubnetId"`
OwnerID string `json:"OwnerId"`
TagSet []interface{} `json:"TagSet"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"NetworkInterfaces"`
}
type DescribeInstances struct {
Reservations []struct {
Instances []struct {
AmiLaunchIndex int `json:"AmiLaunchIndex"`
ImageID string `json:"ImageId"`
InstanceID string `json:"InstanceId"`
InstanceType string `json:"InstanceType"`
KernelID string `json:"KernelId"`
KeyName string `json:"KeyName"`
LaunchTime string `json:"LaunchTime"`
Monitoring struct {
State string `json:"State"`
} `json:"Monitoring"`
Placement struct {
AvailabilityZone string `json:"AvailabilityZone"`
GroupName string `json:"GroupName"`
Tenancy string `json:"Tenancy"`
} `json:"Placement"`
Platform string `json:"Platform"`
PrivateDNS string `json:"PrivateDnsName"`
PrivateIP string `json:"PrivateIpAddress"`
PublicDNS string `json:"PublicDnsName"`
PublicIP string `json:"PublicIpAddress"`
State string `json:"State"`
SubnetID string `json:"SubnetId"`
VpcID string `json:"VpcId"`
Architecture string `json:"Architecture"`
BlockDeviceMappings []struct {
DeviceName string `json:"DeviceName"`
Ebs struct {
AttachTime time.Time `json:"AttachTime"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
Status string `json:"Status"`
VolumeID string `json:"VolumeId"`
} `json:"Ebs"`
} `json:"BlockDeviceMappings"`
ClientToken string `json:"ClientToken"`
EbsOptimized bool `json:"EbsOptimized"`
Hypervisor string `json:"Hypervisor"`
IamInstanceProfile struct {
Arn string `json:"Arn"`
ID string `json:"Id"`
} `json:"IamInstanceProfile"`
NetworkInterfaces []struct {
Attachment struct {
AttachTime time.Time `json:"AttachTime"`
AttachmentID string `json:"AttachmentId"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
DeviceIndex int `json:"DeviceIndex"`
Status string `json:"Status"`
} `json:"Attachment"`
Description string `json:"Description"`
Groups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"Groups"`
MacAddress string `json:"MacAddress"`
NetworkInterfaceID string `json:"NetworkInterfaceId"`
OwnerID string `json:"OwnerId"`
PrivateDNSName string `json:"PrivateDnsName"`
PrivateIPAddress string `json:"PrivateIpAddress"`
PrivateIPAddresses []struct {
Association struct {
IPOwnerID string `json:"IpOwnerId"`
PublicDNSName string `json:"PublicDnsName"`
PublicIP string `json:"PublicIp"`
} `json:"Association"`
Primary bool `json:"Primary"`
PrivateDNSName string `json:"PrivateDnsName"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"PrivateIpAddresses"`
SourceDestCheck bool `json:"SourceDestCheck"`
Status string `json:"Status"`
SubnetID string `json:"SubnetId"`
VpcID string `json:"VpcId"`
} `json:"NetworkInterfaces"`
RootDeviceName string `json:"RootDeviceName"`
RootDeviceType string `json:"RootDeviceType"`
SecurityGroups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"SecurityGroups"`
SourceDestCheck bool `json:"SourceDestCheck"`
StateReason struct {
Code string `json:"Code"`
Message string `json:"Message"`
} `json:"StateReason"`
Tags []struct {
Key string `json:"Key"`
Value string `json:"Value"`
} `json:"Tags"`
VirtualizationType string `json:"VirtualizationType"`
} `json:"Instances"`
OwnerID string `json:"OwnerId"`
RequesterID string `json:"RequesterId"`
ReservationID string `json:"ReservationId"`
} `json:"Reservations"`
}
type DescribeImages struct {
Images []struct {
Architecture string `json:"Architecture"`
BlockDeviceMappings []struct {
DeviceName string `json:"DeviceName"`
Ebs struct {
DeleteOnTermination bool `json:"DeleteOnTermination"`
SnapshotID string `json:"SnapshotId"`
VolumeSize int `json:"VolumeSize"`
VolumeType string `json:"VolumeType"`
} `json:"Ebs"`
} `json:"BlockDeviceMappings"`
CreationDate string `json:"CreationDate"`
Description string `json:"Description"`
EnaSupport bool `json:"EnaSupport"`
Hypervisor string `json:"Hypervisor"`
ImageID string `json:"ImageId"`
ImageLocation string `json:"ImageLocation"`
ImageType string `json:"ImageType"`
KernelID string `json:"KernelId"`
Name string `json:"Name"`
OwnerAlias string `json:"OwnerAlias"`
OwnerID string `json:"OwnerId"`
Platform string `json:"Platform"`
ProductCodes []struct {
ProductCodeID string `json:"ProductCodeId"`
ProductCodeType string `json:"ProductCodeType"`
} `json:"ProductCodes"`
Public bool `json:"Public"`
RamdiskID string `json:"RamdiskId"`
RootDeviceName string `json:"RootDeviceName"`
RootDeviceType string `json:"RootDeviceType"`
SriovNetSupport string `json:"SriovNetSupport"`
State string `json:"State"`
StateReason struct {
Code string `json:"Code"`
Message string `json:"Message"`
} `json:"StateReason"`
Tags []struct {
Key string `json:"Key"`
Value string `json:"Value"`
} `json:"Tags"`
VirtualizationType string `json:"VirtualizationType"`
} `json:"Images"`
NextToken string `json:"NextToken"`
}
type DescribeSnapshots struct {
Snapshots []struct {
DataEncryptionKeyID string `json:"DataEncryptionKeyId"`
Description string `json:"Description"`
Encrypted bool `json:"Encrypted"`
KMSKeyID string `json:"KmsKeyId"`
OwnerAlias string `json:"OwnerAlias"`
OwnerID string `json:"OwnerId"`
Progress string `json:"Progress"`
SnapshotID string `json:"SnapshotId"`
StartTime time.Time `json:"StartTime"`
State string `json:"State"`
StateMessage string `json:"StateMessage"`
TagSet []struct {
Key string `json:"Key"`
Value string `json:"Value"`
} `json:"Tags"`
VolumeID string `json:"VolumeId"`
VolumeSize int `json:"VolumeSize"`
} `json:"Snapshots"`
}
type DescribeVolumes struct {
Volumes []struct {
Attachments []struct {
AttachTime time.Time `json:"AttachTime"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
Device string `json:"Device"`
InstanceID string `json:"InstanceId"`
State string `json:"State"`
VolumeID string `json:"VolumeId"`
} `json:"Attachments"`
AvailabilityZone string `json:"AvailabilityZone"`
CreateTime string `json:"CreateTime"`
Encrypted bool `json:"Encrypted"`
KMSKeyID string `json:"KmsKeyId"`
Size int `json:"Size"`
SnapshotID string `json:"SnapshotId"`
State string `json:"State"`
VolumeID string `json:"VolumeId"`
} `json:"Volumes"`
}
// DescribeInstanceAttribute is a struct for the DescribeInstanceAttribute function - include userdata
type DescribeInstanceAttribute struct {
UserData string `json:"UserData"`
InstanceID string `json:"InstanceId"`
}
type DescribeVpcEndpoints struct {
}
func (c *MockedEC2Client2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
var nics []ec2types.NetworkInterface
err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
}
for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockednic.Association.PublicIP),
},
NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID),
PrivateIpAddress: aws.String(mockednic.PrivateIPAddress),
VpcId: aws.String(mockednic.VpcID),
Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)},
Description: aws.String(mockednic.Description),
})
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
func (c *MockedEC2Client2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
var instances []ec2types.Instance
err := json.Unmarshal(readTestFile(DESCRIBE_INSTANCES_TEST_FILE), &c.describeInstances)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_INSTANCES_TEST_FILE)
}
for _, mockedReservation := range c.describeInstances.Reservations {
for _, mockedInstance := range mockedReservation.Instances {
for _, inputInstanceID := range input.InstanceIds {
if mockedInstance.InstanceID == inputInstanceID {
instances = append(instances, ec2types.Instance{
InstanceId: aws.String(mockedInstance.InstanceID),
InstanceType: ec2types.InstanceType(mockedInstance.InstanceType),
ImageId: aws.String(mockedInstance.ImageID),
})
}
}
}
}
return &ec2.DescribeInstancesOutput{Reservations: []ec2types.Reservation{
{Instances: instances},
}}, nil
}
func (c *MockedEC2Client2) DescribeVolumes(ctx context.Context, input *ec2.DescribeVolumesInput, f ...func(o *ec2.Options)) (*ec2.DescribeVolumesOutput, error) {
var volumes []ec2types.Volume
err := json.Unmarshal(readTestFile(DESCRIBE_VOLUMES_TEST_FILE), &c.describeVolumes)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_VOLUMES_TEST_FILE)
}
for _, mockedVolume := range c.describeVolumes.Volumes {
for _, inputVolumeID := range input.VolumeIds {
if mockedVolume.VolumeID == inputVolumeID {
volumes = append(volumes, ec2types.Volume{
VolumeId: aws.String(mockedVolume.VolumeID),
Size: aws.Int32(int32(mockedVolume.Size)),
Attachments: []ec2types.VolumeAttachment{
{
InstanceId: aws.String(mockedVolume.Attachments[0].InstanceID),
},
},
})
}
}
}
return &ec2.DescribeVolumesOutput{Volumes: volumes}, nil
}
func (c *MockedEC2Client2) DescribeSnapshots(ctx context.Context, input *ec2.DescribeSnapshotsInput, f ...func(o *ec2.Options)) (*ec2.DescribeSnapshotsOutput, error) {
var snapshots []ec2types.Snapshot
err := json.Unmarshal(readTestFile(DESCRIBE_SNAPSHOTS_TEST_FILE), &c.describeSnapshots)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_SNAPSHOTS_TEST_FILE)
}
for _, mockedSnapshot := range c.describeSnapshots.Snapshots {
for _, inputSnapshotID := range input.SnapshotIds {
if mockedSnapshot.SnapshotID == inputSnapshotID {
snapshots = append(snapshots, ec2types.Snapshot{
SnapshotId: aws.String(mockedSnapshot.SnapshotID),
VolumeId: aws.String(mockedSnapshot.VolumeID),
State: ec2types.SnapshotState(mockedSnapshot.State),
})
}
}
}
return &ec2.DescribeSnapshotsOutput{Snapshots: snapshots}, nil
}
func (c *MockedEC2Client2) DescribeImages(ctx context.Context, input *ec2.DescribeImagesInput, f ...func(o *ec2.Options)) (*ec2.DescribeImagesOutput, error) {
var images []ec2types.Image
err := json.Unmarshal(readTestFile(DESCRIBE_IMAGES_TEST_FILE), &c.describeImages)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_IMAGES_TEST_FILE)
}
for _, mockedImage := range c.describeImages.Images {
for _, inputImageID := range input.ImageIds {
if mockedImage.ImageID == inputImageID {
images = append(images, ec2types.Image{
ImageId: aws.String(mockedImage.ImageID),
Name: aws.String(mockedImage.Name),
})
}
}
}
return &ec2.DescribeImagesOutput{Images: images}, nil
}
func (c *MockedEC2Client2) DescribeInstanceAttribute(ctx context.Context, input *ec2.DescribeInstanceAttributeInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error) {
return &ec2.DescribeInstanceAttributeOutput{
UserData: &ec2types.AttributeValue{
Value: aws.String("userdata"),
},
}, nil
}
func (c *MockedEC2Client2) DescribeVpcEndpoints(ctx context.Context, input *ec2.DescribeVpcEndpointsInput, f ...func(o *ec2.Options)) (*ec2.DescribeVpcEndpointsOutput, error) {
return &ec2.DescribeVpcEndpointsOutput{
VpcEndpoints: []ec2types.VpcEndpoint{
{
VpcEndpointId: aws.String("vpce-1234567890abcdefg"),
PolicyDocument: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
]
}`),
},
{
VpcEndpointId: aws.String("vpce-1234567890abcdefh"),
PolicyDocument: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:SourceVpce": "vpce-1234567890abcdefg"
}
}
}
]
}`),
},
},
}, nil
}
+175
View File
@@ -0,0 +1,175 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"strings"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSECRClientInterface interface {
DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error)
DescribeImages(ctx context.Context, params *ecr.DescribeImagesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error)
GetRepositoryPolicy(ctx context.Context, params *ecr.GetRepositoryPolicyInput, optFns ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error)
}
func init() {
gob.Register([]ecrTypes.Repository{})
gob.Register([]ecrTypes.ImageDetail{})
gob.Register(ecrTypes.Repository{})
}
// create a CachedECRDescribeRepositories function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID string, region string) ([]ecrTypes.Repository, error) {
var PaginationControl *string
var repositories []ecrTypes.Repository
cacheKey := fmt.Sprintf("%s-ecr-DescribeRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeRepositories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ecrTypes.Repository), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeRepositories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeRepositories, err := ECRClient.DescribeRepositories(
context.TODO(),
&ecr.DescribeRepositoriesInput{
NextToken: PaginationControl,
},
func(o *ecr.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
repositories = append(repositories, DescribeRepositories.Repositories...)
// Pagination control.
if DescribeRepositories.NextToken != nil {
PaginationControl = DescribeRepositories.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
return repositories, nil
}
// create a CachedECRDescribeImages function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
func CachedECRDescribeImages(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) ([]ecrTypes.ImageDetail, error) {
var PaginationControl *string
var images []ecrTypes.ImageDetail
cacheKey := fmt.Sprintf("%s-ecr-DescribeImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeImages",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ecrTypes.ImageDetail), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeImages",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeImages, err := ECRClient.DescribeImages(
context.TODO(),
&ecr.DescribeImagesInput{
NextToken: PaginationControl,
RepositoryName: &repositoryName,
},
func(o *ecr.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
images = append(images, DescribeImages.ImageDetails...)
// Pagination control.
if DescribeImages.NextToken != nil {
PaginationControl = DescribeImages.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, images, cache.DefaultExpiration)
return images, nil
}
// create a CachedECRGetRepositoryPolicy function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
func CachedECRGetRepositoryPolicy(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) (string, error) {
// in repositoryName, replace "/" with "-"
cacheKey := fmt.Sprintf("%s-ecr-GetRepositoryPolicy-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:GetRepositoryPolicy",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:GetRepositoryPolicy",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "miss",
}).Info("AWS API call")
GetRepositoryPolicy, err := ECRClient.GetRepositoryPolicy(
context.TODO(),
&ecr.GetRepositoryPolicyInput{
RepositoryName: &repositoryName,
},
func(o *ecr.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return "", err
}
PolicyText := aws.ToString(GetRepositoryPolicy.PolicyText)
internal.Cache.Set(cacheKey, PolicyText, cache.DefaultExpiration)
return PolicyText, nil
}
+109
View File
@@ -0,0 +1,109 @@
package sdk
import (
"context"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
)
type MockedECRClient struct {
}
func (m *MockedECRClient) DescribeRepositories(ctx context.Context, input *ecr.DescribeRepositoriesInput, options ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
return &ecr.DescribeRepositoriesOutput{
Repositories: []ecrTypes.Repository{
{
RepositoryName: aws.String("repo1"),
RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1"),
},
{
RepositoryName: aws.String("repo2"),
RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2"),
},
},
}, nil
}
func (m *MockedECRClient) DescribeImages(ctx context.Context, input *ecr.DescribeImagesInput, options ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
if aws.ToString(input.RepositoryName) == "repo1" {
return &ecr.DescribeImagesOutput{
ImageDetails: []ecrTypes.ImageDetail{
{
ImageTags: []string{
"customtag",
"tag2",
},
ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(123456),
},
},
}, nil
} else if aws.ToString(input.RepositoryName) == "repo2" {
return &ecr.DescribeImagesOutput{
ImageDetails: []ecrTypes.ImageDetail{
{
ImageTags: []string{
"latest",
},
ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(2222222),
},
},
}, nil
} else {
return &ecr.DescribeImagesOutput{
ImageDetails: []ecrTypes.ImageDetail{
{
ImageTags: []string{
"customtag",
"tag2",
},
ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(111),
},
{
ImageTags: []string{
"latest",
},
ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(333),
},
},
}, nil
}
}
func (m *MockedECRClient) GetRepositoryPolicy(ctx context.Context, input *ecr.GetRepositoryPolicyInput, options ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) {
return &ecr.GetRepositoryPolicyOutput{
PolicyText: aws.String(`{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowPushPull",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:root",
"arn:aws:iam::123456789012:user/MyUser"
]
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}
]
}`),
}, nil
}
+286
View File
@@ -0,0 +1,286 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"strings"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSECSClientInterface interface {
ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error)
ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error)
ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error)
DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error)
DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
}
func init() {
gob.Register([]string{})
gob.Register(ecsTypes.Task{})
gob.Register([]ecsTypes.Task{})
gob.Register(ecsTypes.TaskDefinition{})
}
func CachedECSListClusters(ECSClient AWSECSClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var clusters []string
cacheKey := fmt.Sprintf("%s-ecs-ListClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListClusters, err := ECSClient.ListClusters(
context.TODO(),
&ecs.ListClustersInput{
NextToken: PaginationControl,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
clusters = append(clusters, ListClusters.ClusterArns...)
// Pagination control.
if ListClusters.NextToken != nil {
PaginationControl = ListClusters.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
func CachedECSListTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) {
var PaginationControl *string
var tasks []string
//grab cluster name from AWS ARN
clusterName := cluster[strings.LastIndex(cluster, "/")+1:]
cacheKey := fmt.Sprintf("%s-ecs-ListTasks-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListTasks",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListTasks",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
for {
ListTasks, err := ECSClient.ListTasks(
context.TODO(),
&ecs.ListTasksInput{
Cluster: &cluster,
NextToken: PaginationControl,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
tasks = append(tasks, ListTasks.TaskArns...)
// Pagination control.
if ListTasks.NextToken != nil {
PaginationControl = ListTasks.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, tasks, cache.DefaultExpiration)
return tasks, nil
}
func CachedECSDescribeTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string, tasks []string) ([]ecsTypes.Task, error) {
var taskDetails []ecsTypes.Task
//replace semi-colons with underscores in task definition name
clusterFileSystemSafe := strings.ReplaceAll(cluster, ":", "_")
clusterFileSystemSafe = strings.ReplaceAll(clusterFileSystemSafe, "/", "_")
cacheKey := fmt.Sprintf("%s-ecs-DescribeTasks-%s-%s", accountID, region, clusterFileSystemSafe)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTasks",
"account": accountID,
"region": region,
"cluster": cluster,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ecsTypes.Task), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTasks",
"account": accountID,
"region": region,
"cluster": cluster,
"cache": "miss",
}).Info("AWS API call")
DescribeTasks, err := ECSClient.DescribeTasks(
context.TODO(),
&ecs.DescribeTasksInput{
Cluster: &cluster,
Tasks: tasks,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return []ecsTypes.Task{}, err
}
taskDetails = append(taskDetails, DescribeTasks.Tasks...)
internal.Cache.Set(cacheKey, taskDetails, cache.DefaultExpiration)
return taskDetails, nil
}
func CachedECSDescribeTaskDefinition(ECSClient AWSECSClientInterface, accountID string, region string, taskDefinition string) (ecsTypes.TaskDefinition, error) {
var taskDefinitionDetails ecsTypes.TaskDefinition
//replace semi-colons with underscores in task definition name
taskDefinitionFileSystemSafe := strings.ReplaceAll(taskDefinition, ":", "_")
taskDefinitionFileSystemSafe = strings.ReplaceAll(taskDefinitionFileSystemSafe, "/", "_")
cacheKey := fmt.Sprintf("%s-ecs-DescribeTaskDefinition-%s-%s", accountID, region, taskDefinitionFileSystemSafe)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTaskDefinition",
"account": accountID,
"region": region,
"taskDefinition": taskDefinition,
"cache": "hit",
}).Info("AWS API call")
return cached.(ecsTypes.TaskDefinition), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTaskDefinition",
"account": accountID,
"region": region,
"taskDefinition": taskDefinition,
"cache": "miss",
}).Info("AWS API call")
DescribeTaskDefinition, err := ECSClient.DescribeTaskDefinition(
context.TODO(),
&ecs.DescribeTaskDefinitionInput{
TaskDefinition: &taskDefinition,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return ecsTypes.TaskDefinition{}, err
}
taskDefinitionDetails = *DescribeTaskDefinition.TaskDefinition
internal.Cache.Set(cacheKey, taskDefinitionDetails, cache.DefaultExpiration)
return taskDefinitionDetails, nil
}
func CachedECSListServices(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) {
var PaginationControl *string
var services []string
//grab cluster name from AWS ARN
clusterName := cluster[strings.LastIndex(cluster, "/")+1:]
cacheKey := fmt.Sprintf("%s-ecs-ListServices-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListServices",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListServices",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
for {
ListServices, err := ECSClient.ListServices(
context.TODO(),
&ecs.ListServicesInput{
Cluster: &cluster,
NextToken: PaginationControl,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
services = append(services, ListServices.ServiceArns...)
// Pagination control.
if ListServices.NextToken != nil {
PaginationControl = ListServices.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
return services, nil
}
+174
View File
@@ -0,0 +1,174 @@
package sdk
import (
"context"
"encoding/json"
"log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
)
const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json"
type MockedECSClient struct {
describeTasks DescribeTasks
}
type ListTasks struct {
TaskArns []string `json:"taskArns"`
}
type DescribeTasks struct {
Tasks []struct {
Attachments []struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
Details []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"details"`
} `json:"attachments"`
Attributes []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"attributes"`
AvailabilityZone string `json:"availabilityZone"`
ClusterArn string `json:"clusterArn"`
Connectivity string `json:"connectivity"`
ConnectivityAt string `json:"connectivityAt"`
Containers []struct {
ContainerArn string `json:"containerArn"`
TaskArn string `json:"taskArn"`
Name string `json:"name"`
Image string `json:"image"`
RuntimeID string `json:"runtimeId"`
LastStatus string `json:"lastStatus"`
NetworkBindings []interface{} `json:"networkBindings"`
NetworkInterfaces []struct {
AttachmentID string `json:"attachmentId"`
PrivateIpv4Address string `json:"privateIpv4Address"`
} `json:"networkInterfaces"`
HealthStatus string `json:"healthStatus"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
} `json:"containers"`
CPU string `json:"cpu"`
CreatedAt string `json:"createdAt"`
DesiredStatus string `json:"desiredStatus"`
EnableExecuteCommand bool `json:"enableExecuteCommand"`
Group string `json:"group"`
HealthStatus string `json:"healthStatus"`
LastStatus string `json:"lastStatus"`
LaunchType string `json:"launchType"`
Memory string `json:"memory"`
Overrides struct {
ContainerOverrides []struct {
Name string `json:"name"`
} `json:"containerOverrides"`
InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"`
} `json:"overrides"`
PlatformVersion string `json:"platformVersion"`
PlatformFamily string `json:"platformFamily"`
PullStartedAt string `json:"pullStartedAt"`
PullStoppedAt string `json:"pullStoppedAt"`
StartedAt string `json:"startedAt"`
StartedBy string `json:"startedBy"`
Tags []interface{} `json:"tags"`
TaskArn string `json:"taskArn"`
TaskDefinitionArn string `json:"taskDefinitionArn"`
Version int `json:"version"`
EphemeralStorage struct {
SizeInGiB int `json:"sizeInGiB"`
} `json:"ephemeralStorage"`
} `json:"tasks"`
Failures []interface{} `json:"failures"`
}
func (c *MockedECSClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{ClusterArns: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3",
}}, nil
}
func (c *MockedECSClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
return &ecs.ListTasksOutput{TaskArns: []string{
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
}}, nil
}
func (c *MockedECSClient) ListServices(ctx context.Context, input *ecs.ListServicesInput, f ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) {
return &ecs.ListServicesOutput{ServiceArns: []string{
"arn:aws:ecs:us-east-1:123456789012:service/MyService",
"arn:aws:ecs:us-east-1:123456789012:service/MyService2",
"arn:aws:ecs:us-east-1:123456789012:service/MyService3",
}}, nil
}
func (c *MockedECSClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE)
}
var tasks []ecsTypes.Task
for _, mockedTask := range c.describeTasks.Tasks {
if mockedTask.ClusterArn == aws.ToString(input.Cluster) {
for _, inputTask := range input.Tasks {
if mockedTask.TaskArn == inputTask {
var attachments []ecsTypes.Attachment
for _, a := range mockedTask.Attachments {
var deets []ecsTypes.KeyValuePair
for _, detail := range a.Details {
deets = append(deets, ecsTypes.KeyValuePair{
Name: aws.String(detail.Name),
Value: aws.String(detail.Value)})
}
attachments = append(attachments, ecsTypes.Attachment{
Type: aws.String(a.Type),
Details: deets,
Id: aws.String(a.ID),
Status: aws.String(a.Status),
})
}
var containers []ecsTypes.Container
for _, container := range mockedTask.Containers {
containers = append(containers, ecsTypes.Container{
ContainerArn: aws.String(container.ContainerArn),
Cpu: aws.String(container.CPU),
HealthStatus: ecsTypes.HealthStatus(container.HealthStatus),
Image: aws.String(container.Image),
LastStatus: aws.String(container.LastStatus),
Memory: aws.String(container.Memory),
Name: aws.String(container.Name),
RuntimeId: aws.String(container.RuntimeID),
TaskArn: aws.String(container.TaskArn),
})
}
tasks = append(tasks, ecsTypes.Task{
ClusterArn: aws.String(mockedTask.ClusterArn),
TaskDefinitionArn: aws.String(mockedTask.TaskDefinitionArn),
LaunchType: ecsTypes.LaunchType(*aws.String(mockedTask.LaunchType)),
TaskArn: aws.String(mockedTask.TaskArn),
Attachments: attachments,
Containers: containers,
})
}
}
}
}
return &ecs.DescribeTasksOutput{Tasks: tasks}, nil
}
func (c *MockedECSClient) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) {
testTaskDefinition := ecsTypes.TaskDefinition{}
testTaskDefinition.TaskRoleArn = aws.String("test123")
return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil
}
+227
View File
@@ -0,0 +1,227 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/efs"
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSEFSClientInterface interface {
DescribeFileSystems(ctx context.Context, params *efs.DescribeFileSystemsInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error)
DescribeMountTargets(ctx context.Context, params *efs.DescribeMountTargetsInput, optFns ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error)
DescribeAccessPoints(ctx context.Context, params *efs.DescribeAccessPointsInput, optFns ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error)
DescribeFileSystemPolicy(ctx context.Context, params *efs.DescribeFileSystemPolicyInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemPolicyOutput, error)
}
func init() {
gob.Register([]efsTypes.FileSystemDescription{})
gob.Register([]efsTypes.MountTargetDescription{})
gob.Register([]efsTypes.AccessPointDescription{})
gob.Register(policy.Policy{})
}
func CachedDescribeFileSystems(EFSClient AWSEFSClientInterface, accountID string, r string) ([]efsTypes.FileSystemDescription, error) {
var PaginationMarker *string
var filesystems []efsTypes.FileSystemDescription
var err error
cacheKey := fmt.Sprintf("%s-efs-DescribeFileSystems-%s", accountID, r)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystems",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.([]efsTypes.FileSystemDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystems",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeFileSystems, err := EFSClient.DescribeFileSystems(
context.TODO(),
&efs.DescribeFileSystemsInput{
Marker: PaginationMarker,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return nil, err
}
filesystems = append(filesystems, DescribeFileSystems.FileSystems...)
// Pagination control. After the last page of output, the for loop exits.
if DescribeFileSystems.Marker != nil {
PaginationMarker = DescribeFileSystems.Marker
} else {
PaginationMarker = nil
break
}
}
internal.Cache.Set(cacheKey, filesystems, cache.DefaultExpiration)
return filesystems, err
}
func CachedDescribeMountTargets(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.MountTargetDescription, error) {
var PaginationMarker *string
var mountTargets []efsTypes.MountTargetDescription
var err error
cacheKey := fmt.Sprintf("%s-efs-DescribeMountTargets-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeMountTargets",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.([]efsTypes.MountTargetDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeMountTargets",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeMountTargets, err := EFSClient.DescribeMountTargets(
context.TODO(),
&efs.DescribeMountTargetsInput{
FileSystemId: aws.String(filesystemId),
Marker: PaginationMarker,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return nil, err
}
mountTargets = append(mountTargets, DescribeMountTargets.MountTargets...)
// Pagination control. After the last page of output, the for loop exits.
if DescribeMountTargets.Marker != nil {
PaginationMarker = DescribeMountTargets.Marker
} else {
PaginationMarker = nil
break
}
}
internal.Cache.Set(cacheKey, mountTargets, cache.DefaultExpiration)
return mountTargets, err
}
func CachedDescribeAccessPoints(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.AccessPointDescription, error) {
var PaginationMarker *string
var accessPoints []efsTypes.AccessPointDescription
var err error
cacheKey := fmt.Sprintf("%s-efs-DescribeAccessPoints-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeAccessPoints",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.([]efsTypes.AccessPointDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeAccessPoints",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeAccessPoints, err := EFSClient.DescribeAccessPoints(
context.TODO(),
&efs.DescribeAccessPointsInput{
FileSystemId: aws.String(filesystemId),
NextToken: PaginationMarker,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return nil, err
}
accessPoints = append(accessPoints, DescribeAccessPoints.AccessPoints...)
// Pagination control. After the last page of output, the for loop exits.
if DescribeAccessPoints.NextToken != nil {
PaginationMarker = DescribeAccessPoints.NextToken
} else {
PaginationMarker = nil
break
}
}
internal.Cache.Set(cacheKey, accessPoints, cache.DefaultExpiration)
return accessPoints, err
}
func CachedDescribeFileSystemPolicy(EFSClient AWSEFSClientInterface, filesystemId string, r string, accountID string) (policy.Policy, error) {
var efsPolicy policy.Policy
var policyJSON string
cacheKey := fmt.Sprintf("efs-%s-DescribeFileSystemPolicy-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystemPolicy",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.(policy.Policy), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystemPolicy",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
Policy, err := EFSClient.DescribeFileSystemPolicy(
context.TODO(),
&efs.DescribeFileSystemPolicyInput{
FileSystemId: aws.String(filesystemId),
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return efsPolicy, err
}
policyJSON = aws.ToString(Policy.Policy)
efsPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
return efsPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", filesystemId, err)
}
internal.Cache.Set(cacheKey, efsPolicy, cache.DefaultExpiration)
return efsPolicy, nil
}
+67
View File
@@ -0,0 +1,67 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/efs"
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
)
type MockedEfsClient struct {
}
func (m *MockedEfsClient) DescribeFileSystems(ctx context.Context, input *efs.DescribeFileSystemsInput, options ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error) {
return &efs.DescribeFileSystemsOutput{
FileSystems: []efsTypes.FileSystemDescription{
{
FileSystemId: aws.String("fs-12345678"),
},
{
FileSystemId: aws.String("fs-87654321"),
},
},
}, nil
}
func (m *MockedEfsClient) DescribeMountTargets(ctx context.Context, input *efs.DescribeMountTargetsInput, options ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error) {
return &efs.DescribeMountTargetsOutput{
MountTargets: []efsTypes.MountTargetDescription{
{
MountTargetId: aws.String("fsmt-12345678"),
FileSystemId: aws.String("fs-12345678"),
IpAddress: aws.String("10.1.1.1"),
},
{
MountTargetId: aws.String("fsmt-87654321"),
FileSystemId: aws.String("fs-87654321"),
IpAddress: aws.String("10.2.2.2.2"),
},
},
}, nil
}
func (m *MockedEfsClient) DescribeAccessPoints(ctx context.Context, input *efs.DescribeAccessPointsInput, options ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error) {
return &efs.DescribeAccessPointsOutput{
AccessPoints: []efsTypes.AccessPointDescription{
{
AccessPointId: aws.String("fsap-12345678"),
FileSystemId: aws.String("fs-12345678"),
Name: aws.String("fsap-12345678"),
PosixUser: &efsTypes.PosixUser{
Gid: aws.Int64(1000),
Uid: aws.Int64(1000),
},
},
{
AccessPointId: aws.String("fsap-87654321"),
FileSystemId: aws.String("fs-87654321"),
Name: aws.String("fsap-12345679"),
PosixUser: &efsTypes.PosixUser{
Gid: aws.Int64(1000),
Uid: aws.Int64(1000),
},
},
},
}, nil
}
+209
View File
@@ -0,0 +1,209 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/eks"
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type EKSClientInterface interface {
DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error)
ListClusters(context.Context, *eks.ListClustersInput, ...func(*eks.Options)) (*eks.ListClustersOutput, error)
DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error)
ListNodegroups(context.Context, *eks.ListNodegroupsInput, ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error)
}
func init() {
gob.Register([]string{})
gob.Register(eksTypes.Cluster{})
gob.Register(eksTypes.Nodegroup{})
}
func CachedEKSDescribeCluster(client EKSClientInterface, accountID string, clusterName string, region string) (eksTypes.Cluster, error) {
var cluster eksTypes.Cluster
cacheKey := fmt.Sprintf("%s-eks-DescribeCluster-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeCluster",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.(eksTypes.Cluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeCluster",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
clusterOutput, err := client.DescribeCluster(
context.TODO(),
&eks.DescribeClusterInput{
Name: &clusterName,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return cluster, err
}
cluster = *clusterOutput.Cluster
internal.Cache.Set(cacheKey, cluster, cache.DefaultExpiration)
return cluster, nil
}
func CachedEKSListClusters(client EKSClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var clusters []string
cacheKey := fmt.Sprintf("%s-eks-ListClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListClusters, err := client.ListClusters(
context.TODO(),
&eks.ListClustersInput{
NextToken: PaginationControl,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return clusters, err
}
clusters = append(clusters, ListClusters.Clusters...)
//pagination
if ListClusters.NextToken == nil {
break
}
PaginationControl = ListClusters.NextToken
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
func CachedEKSListNodeGroups(client EKSClientInterface, accountID string, region string, clusterName string) ([]string, error) {
var PaginationControl *string
var nodeGroups []string
cacheKey := fmt.Sprintf("%s-eks-ListNodeGroups-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListNodegroups",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListNodegroups",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
for {
ListNodeGroups, err := client.ListNodegroups(
context.TODO(),
&eks.ListNodegroupsInput{
ClusterName: &clusterName,
NextToken: PaginationControl,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return nodeGroups, err
}
nodeGroups = append(nodeGroups, ListNodeGroups.Nodegroups...)
if ListNodeGroups.NextToken != nil {
PaginationControl = ListNodeGroups.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, nodeGroups, cache.DefaultExpiration)
return nodeGroups, nil
}
func CachedEKSDescribeNodeGroup(client EKSClientInterface, accountID string, region string, clusterName string, nodeGroupName string) (eksTypes.Nodegroup, error) {
var nodeGroup eksTypes.Nodegroup
cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, clusterName, nodeGroupName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeNodegroup",
"account": accountID,
"region": region,
"cluster": clusterName,
"nodeGroup": nodeGroupName,
"cache": "hit",
}).Info("AWS API call")
return cached.(eksTypes.Nodegroup), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeNodegroup",
"account": accountID,
"region": region,
"cluster": clusterName,
"nodeGroup": nodeGroupName,
"cache": "miss",
}).Info("AWS API call")
nodeGroupOutput, err := client.DescribeNodegroup(
context.TODO(),
&eks.DescribeNodegroupInput{
ClusterName: &clusterName,
NodegroupName: &nodeGroupName,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return nodeGroup, err
}
nodeGroup = *nodeGroupOutput.Nodegroup
internal.Cache.Set(cacheKey, nodeGroup, cache.DefaultExpiration)
return nodeGroup, nil
}
+54
View File
@@ -0,0 +1,54 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/eks"
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
)
type MockedAWSEksClient struct {
}
func (m *MockedAWSEksClient) ListClusters(ctx context.Context, input *eks.ListClustersInput, options ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
return &eks.ListClustersOutput{
Clusters: []string{
"cluster1",
"cluster2",
},
}, nil
}
func (m *MockedAWSEksClient) DescribeCluster(ctx context.Context, input *eks.DescribeClusterInput, options ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
return &eks.DescribeClusterOutput{
Cluster: &eksTypes.Cluster{
Arn: aws.String("arn:aws:eks:us-east-1:123456789012:cluster/cluster1"),
Endpoint: aws.String("https://cluster1.us-east-1.eks.amazonaws.com"),
Name: aws.String("cluster1"),
PlatformVersion: aws.String("eks.1"),
Version: aws.String("1.18"),
RoleArn: aws.String("arn:aws:iam::123456789012:role/eks-role"),
},
}, nil
}
func (m *MockedAWSEksClient) ListNodegroups(ctx context.Context, input *eks.ListNodegroupsInput, options ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
return &eks.ListNodegroupsOutput{
Nodegroups: []string{
"nodegroup1",
"nodegroup2",
},
}, nil
}
func (m *MockedAWSEksClient) DescribeNodegroup(ctx context.Context, input *eks.DescribeNodegroupInput, options ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
return &eks.DescribeNodegroupOutput{
Nodegroup: &eksTypes.Nodegroup{
ClusterName: aws.String("cluster1"),
NodeRole: aws.String("arn:aws:iam::123456789012:role/eks-role"),
NodegroupName: aws.String("nodegroup1"),
NodegroupArn: aws.String("arn:aws:eks:us-east-1:123456789012:nodegroup/cluster1/nodegroup1"),
},
}, nil
}
+70
View File
@@ -0,0 +1,70 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/elasticache"
elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSElastiCacheClientInterface interface {
DescribeCacheClusters(context.Context, *elasticache.DescribeCacheClustersInput, ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error)
}
func init() {
gob.Register([]elasticacheTypes.CacheCluster{})
}
func CachedElastiCacheDescribeCacheClusters(client AWSElastiCacheClientInterface, accountID string, region string) ([]elasticacheTypes.CacheCluster, error) {
var PaginationControl *string
var clusters []elasticacheTypes.CacheCluster
cacheKey := fmt.Sprintf("%s-elasticache-DescribeCacheClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "elasticache:DescribeCacheClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]elasticacheTypes.CacheCluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "elasticache:DescribeCacheClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeCacheClusters, err := client.DescribeCacheClusters(
context.TODO(),
&elasticache.DescribeCacheClustersInput{
Marker: PaginationControl,
},
func(o *elasticache.Options) {
o.Region = region
},
)
if err != nil {
return clusters, err
}
clusters = append(clusters, DescribeCacheClusters.CacheClusters...)
//pagination
if DescribeCacheClusters.Marker == nil {
break
}
PaginationControl = DescribeCacheClusters.Marker
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
+40
View File
@@ -0,0 +1,40 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/elasticache"
elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
)
type MockedElasticacheClient struct {
}
func (m *MockedElasticacheClient) DescribeCacheClusters(ctx context.Context, input *elasticache.DescribeCacheClustersInput, options ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) {
return &elasticache.DescribeCacheClustersOutput{
CacheClusters: []elasticacheTypes.CacheCluster{
{
CacheClusterId: aws.String("test"),
ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:cluster:myCluster"),
Engine: aws.String("redis"),
EngineVersion: aws.String("6.x"),
CacheNodeType: aws.String("cache.t3.micro"),
NumCacheNodes: aws.Int32(1),
CacheClusterStatus: aws.String("available"),
PreferredAvailabilityZone: aws.String("us-east-1a"),
CacheSubnetGroupName: aws.String("default"),
ReplicationGroupId: aws.String("test"),
SecurityGroups: []elasticacheTypes.SecurityGroupMembership{
{
SecurityGroupId: aws.String("test"),
Status: aws.String("active"),
},
},
AutoMinorVersionUpgrade: aws.Bool(true),
PreferredMaintenanceWindow: aws.String("sun:05:00-sun:06:00"),
},
},
}, nil
}
+57
View File
@@ -0,0 +1,57 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk"
elasticbeanstalkTypes "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSElasticBeanstalkClientInterface interface {
DescribeApplications(context.Context, *elasticbeanstalk.DescribeApplicationsInput, ...func(*elasticbeanstalk.Options)) (*elasticbeanstalk.DescribeApplicationsOutput, error)
}
func init() {
gob.Register([]elasticbeanstalkTypes.ApplicationDescription{})
}
func CachedElasticBeanstalkDescribeApplications(client AWSElasticBeanstalkClientInterface, accountID string, region string) ([]elasticbeanstalkTypes.ApplicationDescription, error) {
var applications []elasticbeanstalkTypes.ApplicationDescription
cacheKey := fmt.Sprintf("%s-elasticbeanstalk-DescribeApplications-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "elasticbeanstalk:DescribeApplications",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]elasticbeanstalkTypes.ApplicationDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "elasticbeanstalk:DescribeApplications",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
DescribeApplications, err := client.DescribeApplications(
context.TODO(),
&elasticbeanstalk.DescribeApplicationsInput{},
func(o *elasticbeanstalk.Options) {
o.Region = region
},
)
if err != nil {
return applications, err
}
applications = append(applications, DescribeApplications.Applications...)
internal.Cache.Set(cacheKey, applications, cache.DefaultExpiration)
return applications, nil
}
Loaded 100 of 364 files, more files were not shown because too many files have changed in this diff. Show more