Skip to content

Commit

Permalink
Merge pull request #222 from duo-labs/update
Browse files Browse the repository at this point in the history
Update
  • Loading branch information
0xdabbad00 committed Sep 5, 2022
2 parents 6908bb3 + c129a2c commit dad4197
Show file tree
Hide file tree
Showing 30 changed files with 57,269 additions and 28,144 deletions.
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[run]
source = parliament
omit = parliament/cli.py

[report]
fail_under = 75
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,9 @@ ignore_locations:
To create unit tests for our new private auditor, create the directory `./parliament/private_auditors/tests/` and create the file `test_sensitive_bucket_access.py` there with the contents:

```
import unittest
from nose.tools import raises, assert_equal
# import parliament
from parliament import analyze_policy_string
class TestCustom(unittest.TestCase):
class TestCustom():
"""Test class for custom auditor"""
def test_my_auditor(self):
Expand Down
46 changes: 26 additions & 20 deletions parliament/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
This library is a linter for AWS IAM policies.
"""
__version__ = "1.5.2"
__version__ = "1.6.0"

import fnmatch
import functools
Expand Down Expand Up @@ -62,7 +62,11 @@ def analyze_policy_string(
policy_json = jsoncfg.loads_config(policy_str)
except jsoncfg.parser.JSONConfigParserException as e:
policy = Policy(None)
policy.add_finding("MALFORMED_JSON", detail="json parsing error: {}".format(e), location={'line': e.line, 'column': e.column})
policy.add_finding(
"MALFORMED_JSON",
detail="json parsing error: {}".format(e),
location={"line": e.line, "column": e.column},
)
return policy

policy = Policy(policy_json, filepath, config)
Expand Down Expand Up @@ -97,7 +101,7 @@ def is_arn_match(resource_type, arn_format, resource):
- arn_format: ARN regex from the docs
- resource: ARN regex from IAM policy
We can cheat some because after the first sections of the arn match, meaning until the 5th colon (with some
rules there to allow empty or asterisk sections), we only need to match the ID part.
So the above is simplified to "*/*" and "*personalize*".
Expand Down Expand Up @@ -147,6 +151,7 @@ def is_arn_match(resource_type, arn_format, resource):
resource_id = re.sub(r"\[.+?\]", "*", resource_id)
return is_glob_match(arn_id, resource_id)


def is_arn_strictly_valid(resource_type, arn_format, resource):
"""
Strictly validate the arn_format specified in the docs, with the resource
Expand Down Expand Up @@ -179,7 +184,7 @@ def is_arn_strictly_valid(resource_type, arn_format, resource):
arn_id_resource_type = re.match(r"(^[^\*][\w-]+)[\/\:].+", arn_id)

if arn_id_resource_type != None and resource_id != "*":

# https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namesspaces
# The following is not allowed: arn:aws:iam::123456789012:u*
if not (resource_id.startswith(arn_id_resource_type[1])):
Expand All @@ -193,9 +198,11 @@ def is_arn_strictly_valid(resource_type, arn_format, resource):
return True
return False


def strip_var_from_arn(arn, replace_with=""):
return re.sub(r"\$\{aws.[\w\/]+\}", replace_with, arn)


def is_glob_match(s1, s2):
# This comes from https://github.com/duo-labs/parliament/issues/36#issuecomment-574001764

Expand Down Expand Up @@ -275,20 +282,20 @@ def expand_action(action, raise_exceptions=True):


def get_resource_type_matches_from_arn(arn):
""" Given an ARN such as "arn:aws:s3:::mybucket", find resource types that match it.
This would return:
[
"resource": {
"arn": "arn:${Partition}:s3:::${BucketName}",
"condition_keys": [],
"resource": "bucket"
},
"service": {
"service_name": "Amazon S3",
"privileges": [...]
...
}
]
"""Given an ARN such as "arn:aws:s3:::mybucket", find resource types that match it.
This would return:
[
"resource": {
"arn": "arn:${Partition}:s3:::${BucketName}",
"condition_keys": [],
"resource": "bucket"
},
"service": {
"service_name": "Amazon S3",
"privileges": [...]
...
}
]
"""
matches = []
for service in iam_definition:
Expand All @@ -300,8 +307,7 @@ def get_resource_type_matches_from_arn(arn):


def get_privilege_matches_for_resource_type(resource_type_matches):
""" Given the response from get_resource_type_matches_from_arn(...), this will identify the relevant privileges.
"""
"""Given the response from get_resource_type_matches_from_arn(...), this will identify the relevant privileges."""
privilege_matches = []
for match in resource_type_matches:
for privilege in match["service"]["privileges"]:
Expand Down
23 changes: 12 additions & 11 deletions parliament/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,12 @@ def main():
help='Provide a string such as \'{"Version": "2012-10-17","Statement": {"Effect": "Allow","Action": ["s3:GetObject", "s3:PutBucketPolicy"],"Resource": ["arn:aws:s3:::bucket1", "arn:aws:s3:::bucket2/*"]}}\'',
type=str,
)
parser.add_argument('--file',
help="Provide a policy via stdin (e.g. through piping) or --file",
type=argparse.FileType('r'),
default=sys.stdin)
parser.add_argument(
"--file",
help="Provide a policy via stdin (e.g. through piping) or --file",
type=argparse.FileType("r"),
default=sys.stdin,
)
parser.add_argument(
"--directory", help="Provide a path to directory with policy files", type=str
)
Expand Down Expand Up @@ -236,9 +238,7 @@ def main():
with open(file_path) as f:
contents = f.read()
policy_file_json = jsoncfg.loads_config(contents)
policy_string = json.dumps(
policy_file_json.PolicyVersion.Document()
)
policy_string = json.dumps(policy_file_json.PolicyVersion.Document())
policy = analyze_policy_string(
policy_string,
file_path,
Expand Down Expand Up @@ -271,15 +271,16 @@ def main():
if not version.IsDefaultVersion():
continue
policy = analyze_policy_string(
json.dumps(version.Document()), policy.Arn(),
json.dumps(version.Document()),
policy.Arn(),
)
findings.extend(policy.findings)

# Review the inline policies on Users, Roles, and Groups
for user in auth_details_json.UserDetailList:
for policy in user.UserPolicyList([]):
policy = analyze_policy_string(
json.dumps(policy['PolicyDocument']),
json.dumps(policy["PolicyDocument"]),
user.Arn(),
private_auditors_custom_path=args.private_auditors,
include_community_auditors=args.include_community_auditors,
Expand All @@ -289,7 +290,7 @@ def main():
for role in auth_details_json.RoleDetailList:
for policy in role.RolePolicyList([]):
policy = analyze_policy_string(
json.dumps(policy['PolicyDocument']),
json.dumps(policy["PolicyDocument"]),
role.Arn(),
private_auditors_custom_path=args.private_auditors,
include_community_auditors=args.include_community_auditors,
Expand All @@ -299,7 +300,7 @@ def main():
for group in auth_details_json.GroupDetailList:
for policy in group.GroupPolicyList([]):
policy = analyze_policy_string(
json.dumps(policy['PolicyDocument']),
json.dumps(policy["PolicyDocument"]),
group.Arn(),
private_auditors_custom_path=args.private_auditors,
include_community_auditors=args.include_community_auditors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from parliament import Policy
from parliament.misc import make_list


def audit(policy: Policy) -> None:
global_single_valued_condition_keys = [
"aws:CalledViaFirst",
Expand Down Expand Up @@ -52,9 +53,12 @@ def audit(policy: Policy) -> None:
operator = condition[0]
condition_block = condition[1]
if re.match(r"^For(All|Any)Values:", operator):
keys = list(k for k,_v in condition_block)
if any(any(re.match(k, key) for k in global_single_valued_condition_keys) for key in keys):
keys = list(k for k, _v in condition_block)
if any(
any(re.match(k, key) for k in global_single_valued_condition_keys)
for key in keys
):
policy.add_finding(
"SINGLE_VALUE_CONDITION_TOO_PERMISSIVE",
detail='Checking a single value conditional key against a set of values results in overly permissive policies.',
detail="Checking a single value conditional key against a set of values results in overly permissive policies.",
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import unittest

from nose.tools import assert_equal

from parliament import analyze_policy_string

S3_STAR_FINDINGS = {"PERMISSIONS_MANAGEMENT_ACTIONS", "RESOURCE_MISMATCH"}


class TestAdvancedPolicyElements(unittest.TestCase):
class TestAdvancedPolicyElements:
def test_notresource_allow(self):
# NotResource is OK with Effect: Deny. This denies access to
# all S3 buckets except Payroll buckets. This example is taken from
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import unittest

from nose.tools import assert_equal

# import parliament
from parliament import analyze_policy_string


class TestCredentialsManagement(unittest.TestCase):
class TestCredentialsManagement:
"""Test class for Credentials Management auditor"""

def test_credentials_management(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import unittest

from nose.tools import assert_equal

# import parliament
from parliament import analyze_policy_string


class TestPermissionsManagement(unittest.TestCase):
class TestPermissionsManagement:
"""Test class for Permissions Management auditor"""

def test_permissions_management(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import unittest

from nose.tools import assert_equal

# import parliament
from parliament import analyze_policy_string


class TestPrivilegeEscalation(unittest.TestCase):
class TestPrivilegeEscalation:
"""Test class for Privilege Escalation auditor"""

def test_privilege_escalation(self):
Expand Down
6 changes: 1 addition & 5 deletions parliament/community_auditors/tests/test_sensitive_access.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import unittest

from nose.tools import assert_equal

from parliament import analyze_policy_string


class TestSensitiveAccess(unittest.TestCase):
class TestSensitiveAccess:
"""Test class for Sensitive access auditor"""

def test_sensitive_access(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import unittest

from nose.tools import assert_equal

from parliament import analyze_policy_string


class TestSensitiveAccess(unittest.TestCase):
class TestSensitiveAccess:
"""Test class for single value condition too permissive auditor"""

example_policy_string = """
{
"Version": "2012-10-17",
Expand Down
4 changes: 2 additions & 2 deletions parliament/finding.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Finding:
""" Class for storing findings """
"""Class for storing findings"""

issue = ""
detail = ""
Expand All @@ -15,5 +15,5 @@ def __init__(self, issue, detail, location):
self.location = location

def __repr__(self):
""" Return a string for printing """
"""Return a string for printing"""
return "{} - {} - {}".format(self.issue, self.detail, self.location)

0 comments on commit dad4197

Please sign in to comment.