ECS task-definition checks fail open: unexamined resources are reported as PASS
Summary
When DescribeTaskDefinition fails for a task definition, EcsService swallows
the exception and leaves container_definitions empty. Every check that
iterates that list then reports PASS, because the checks initialize
report.status = "PASS" and only downgrade to FAIL inside the loop over
containers. An empty list means the loop never executes.
The result is a security check reporting a clean verdict for a resource it never
managed to examine. Because whether a given describe call fails varies between
runs, the same immutable resource can flip verdicts run to run.
Version: 5.22.0 (prowlercloud/prowler:5.22.0), AWS provider.
Reproduction
An account with a large number of ACTIVE task-definition revisions (~4,900 in
our case, accumulated because nothing deregisters old revisions). Run the AWS
provider twice, a few minutes apart, and diff the OCSF output per resource.
Observed across two consecutive runs 20 minutes apart:
|
|
| Task-definition revisions scanned |
4,901 (identical set both runs) |
| Revisions that changed verdict |
2,100 (42.8%) |
| FAIL → PASS |
1,172 |
| PASS → FAIL |
928 |
| Checks affected out of 128 run |
1 |
ECS task-definition revisions are immutable, so no resource actually changed.
The net difference in FAIL count was only −244, which is small enough to look
like ordinary run-to-run variance; the underlying churn is two orders of
magnitude larger.
Ground truth via aws ecs describe-task-definition: of 20 sampled revisions
that ecs_task_definitions_containers_readonly_access reported as PASS,
20 had readonlyRootFilesystem unset — which the check itself treats as
FAIL. All 20 were false passes.
Two revisions of the same family, in the same report, with identical
configuration:
revision 2 -> FAIL "has containers with write access to the root filesystem"
revision 255 -> PASS "does not have containers with write access to the root filesystems"
Cause
prowler/providers/aws/services/ecs/ecs_service.py:
def _describe_task_definition(self, task_definition):
try:
...
container_definitions = response["taskDefinition"]["containerDefinitions"]
for container in container_definitions:
task_definition.container_definitions.append(ContainerDefinition(...))
except Exception as error:
logger.error(...) # swallowed -> container_definitions stays EMPTY
This is fanned out with __threading_call__ over every task definition, so on
a large account it issues thousands of concurrent DescribeTaskDefinition
calls — exactly the conditions under which some will fail.
prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py:
report.status = "PASS" # optimistic default
for container in task_definition.container_definitions: # empty -> body never runs
if not container.readonly_rootfilesystem:
report.status = "FAIL"
Failure to gather evidence is indistinguishable, in the output, from evidence of
compliance.
Scope
Six checks read container_definitions and share the pattern:
ecs_task_definitions_containers_readonly_access
ecs_task_definitions_host_namespace_not_shared
ecs_task_definitions_host_networking_mode_users
ecs_task_definitions_logging_enabled
ecs_task_definitions_no_environment_secrets
ecs_task_definitions_no_privileged_containers
In our account the other five return 100% PASS, so a fail-open PASS is
indistinguishable from a correct PASS and the defect is invisible there rather
than absent. ..._readonly_access is the only one whose true verdict is FAIL
for most resources, which is why it is the only one where the bug is observable
as a flip.
Suggested fix
Two independent changes, either of which prevents the false PASS:
-
Do not let a failed gather look like a successful one. Record that the
describe failed (e.g. a gathered: bool on the model, or leave
container_definitions = None to distinguish "none" from "not retrieved"),
and have the checks skip or report the resource as errored rather than PASS.
-
Make the checks derive PASS rather than default to it. Computing
failed = [c for c in containers if not c.readonly_rootfilesystem] and
setting the status from that still passes an empty list, so this alone is
insufficient — but combined with (1) it makes the intent explicit.
Retrying throttled describes with backoff would reduce the frequency, but the
fail-open would remain: any residual error still yields PASS. The reporting
behavior is the part worth fixing.
Happy to send a PR
If this looks right to you, I'm glad to open a pull request. I'd rather agree on
the approach first, since the fix touches a shared model: option (1) changes
ContainerDefinition/EcsTaskDefinition gathering semantics and so affects all
six checks, and I don't want to guess at how you'd prefer an
evidence-not-gathered resource to surface — skipped, MANUAL, or a new status.
Tell me which shape you want and I'll follow it, tests included.
Notes
Reported per SECURITY.md, which directs functional and check issues to the
public issue tracker rather than the support desk. Account identifiers and
resource names have been generalized; I'm happy to share the OCSF diff privately
if that would help.
This report was researched and written by Claude (Anthropic), working in our
repository. I reviewed it, re-ran the ground-truth describe-task-definition
checks against our own AWS account, and am filing it deliberately — please treat
it as our report rather than as automated output, and hold it to that standard.
Any errors in it are ours to correct, so do push back if something does not
reproduce on your side.
ECS task-definition checks fail open: unexamined resources are reported as PASS
Summary
When
DescribeTaskDefinitionfails for a task definition,EcsServiceswallowsthe exception and leaves
container_definitionsempty. Every check thatiterates that list then reports PASS, because the checks initialize
report.status = "PASS"and only downgrade toFAILinside the loop overcontainers. An empty list means the loop never executes.
The result is a security check reporting a clean verdict for a resource it never
managed to examine. Because whether a given describe call fails varies between
runs, the same immutable resource can flip verdicts run to run.
Version: 5.22.0 (
prowlercloud/prowler:5.22.0), AWS provider.Reproduction
An account with a large number of ACTIVE task-definition revisions (~4,900 in
our case, accumulated because nothing deregisters old revisions). Run the AWS
provider twice, a few minutes apart, and diff the OCSF output per resource.
Observed across two consecutive runs 20 minutes apart:
ECS task-definition revisions are immutable, so no resource actually changed.
The net difference in FAIL count was only −244, which is small enough to look
like ordinary run-to-run variance; the underlying churn is two orders of
magnitude larger.
Ground truth via
aws ecs describe-task-definition: of 20 sampled revisionsthat
ecs_task_definitions_containers_readonly_accessreported as PASS,20 had
readonlyRootFilesystemunset — which the check itself treats asFAIL. All 20 were false passes.
Two revisions of the same family, in the same report, with identical
configuration:
Cause
prowler/providers/aws/services/ecs/ecs_service.py:This is fanned out with
__threading_call__over every task definition, so ona large account it issues thousands of concurrent
DescribeTaskDefinitioncalls — exactly the conditions under which some will fail.
prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py:Failure to gather evidence is indistinguishable, in the output, from evidence of
compliance.
Scope
Six checks read
container_definitionsand share the pattern:ecs_task_definitions_containers_readonly_accessecs_task_definitions_host_namespace_not_sharedecs_task_definitions_host_networking_mode_usersecs_task_definitions_logging_enabledecs_task_definitions_no_environment_secretsecs_task_definitions_no_privileged_containersIn our account the other five return 100% PASS, so a fail-open PASS is
indistinguishable from a correct PASS and the defect is invisible there rather
than absent.
..._readonly_accessis the only one whose true verdict is FAILfor most resources, which is why it is the only one where the bug is observable
as a flip.
Suggested fix
Two independent changes, either of which prevents the false PASS:
Do not let a failed gather look like a successful one. Record that the
describe failed (e.g. a
gathered: boolon the model, or leavecontainer_definitions = Noneto distinguish "none" from "not retrieved"),and have the checks skip or report the resource as errored rather than PASS.
Make the checks derive PASS rather than default to it. Computing
failed = [c for c in containers if not c.readonly_rootfilesystem]andsetting the status from that still passes an empty list, so this alone is
insufficient — but combined with (1) it makes the intent explicit.
Retrying throttled describes with backoff would reduce the frequency, but the
fail-open would remain: any residual error still yields PASS. The reporting
behavior is the part worth fixing.
Happy to send a PR
If this looks right to you, I'm glad to open a pull request. I'd rather agree on
the approach first, since the fix touches a shared model: option (1) changes
ContainerDefinition/EcsTaskDefinitiongathering semantics and so affects allsix checks, and I don't want to guess at how you'd prefer an
evidence-not-gathered resource to surface — skipped,
MANUAL, or a new status.Tell me which shape you want and I'll follow it, tests included.
Notes
Reported per
SECURITY.md, which directs functional and check issues to thepublic issue tracker rather than the support desk. Account identifiers and
resource names have been generalized; I'm happy to share the OCSF diff privately
if that would help.
This report was researched and written by Claude (Anthropic), working in our
repository. I reviewed it, re-ran the ground-truth
describe-task-definitionchecks against our own AWS account, and am filing it deliberately — please treat
it as our report rather than as automated output, and hold it to that standard.
Any errors in it are ours to correct, so do push back if something does not
reproduce on your side.