Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,16 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true

- name: Install Poetry Action
uses: snok/install-poetry@v1
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install Dependencies
run: poetry install
if: steps.cache.outputs.cache-hit != 'true'

- name: Cache Poetry virtualenv
uses: actions/cache@v3
id: cache
with:
path: ~/.virtualenvs
key: poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
poetry-${{ hashFiles('**/poetry.lock') }}
run: uv sync

- name: Test with pytest
run: poetry run pytest --cov .
run: uv run pytest --cov .
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ NOTE: This is a work in progress. IAMSpy currently does not support all features

NOTE: Should you pull newer changes for IAMSpy, it is recommended to re-generate any models that may have been pre-computed. This is because internal representations of data are likely to regularly change whilst IAMSpy is being built out to completion.

IAMSpy can be installed through `poetry install` from a checked out repository. This will set up a python virtualenv to be used with IAMSpy. If you would like to install IAMSpy into another python environment, `pip install /path/to/iamspy`
IAMSpy can be installed through `uv sync` from a checked out repository. This will set up a python virtualenv to be used with IAMSpy. If you would like to install IAMSpy into another python environment, `pip install /path/to/iamspy`

It should be noted that GAADs generated by by the `get-account-authorization-details` might miss some required data. For example, when using permission boundaries, managed policies may not be included in the GAAD that are used by boundaries but nothing else.

Expand Down Expand Up @@ -104,8 +104,8 @@ iamspy who-can s3:GetObject arn:aws:s3:::bucket/object --strict-conditions
## Development

- Checkout the repo
- `poetry install`
- `poetry shell`
- `uv sync`
- `uv run <command>` or activate the venv with `source .venv/bin/activate`

### Code Quality

Expand Down
2 changes: 1 addition & 1 deletion iamspy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from iamspy.model import Model

model = Model()
model = Model()
45 changes: 37 additions & 8 deletions iamspy/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import logging
import os
from typing import List, Optional
import typer
from iamspy.model import Model
from pathlib import Path
from iamspy.log_config import build_logger

DEFAULT_MODEL_FILENAME = "model.json"


app = typer.Typer()


@app.command()
def load_gaad(gaad: str = typer.Argument(...), model: str = typer.Option("model.smt2", "-f")):
def load_gaad(gaad: str = typer.Argument(...), model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f")):
m = Model()
if Path(model).is_file():
m.load_model(model)
Expand All @@ -18,7 +22,16 @@ def load_gaad(gaad: str = typer.Argument(...), model: str = typer.Option("model.


@app.command()
def load_resources(resources: str = typer.Argument(...), model: str = typer.Option("model.smt2", "-f")):
def load_gaads(folder: str = typer.Argument(...), model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f")):
m = Model()
if Path(model).is_file():
m.load_model(model)
m.load_gaads(folder)
m.save(model)


@app.command()
def load_resources(resources: str = typer.Argument(...), model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f")):
m = Model()
if Path(model).is_file():
m.load_model(model)
Expand All @@ -27,7 +40,7 @@ def load_resources(resources: str = typer.Argument(...), model: str = typer.Opti


@app.command()
def load_scps(scps: str = typer.Argument(...), model: str = typer.Option("model.smt2", "-f")):
def load_scps(scps: str = typer.Argument(...), model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f")):
m = Model()
if Path(model).is_file():
m.load_model(model)
Expand All @@ -47,7 +60,7 @@ def can_i(
strict_conditions: bool = typer.Option(
False, help="Whether to require conditions to be passed in for any IAM condition checks"
),
model: str = typer.Option("model.smt2", "-f"),
model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f"),
):
"""
Pulls out applicable policies, runs can_i
Expand All @@ -70,7 +83,8 @@ def who_can(
strict_conditions: bool = typer.Option(
False, help="Whether to require conditions to be passed in for any IAM condition checks"
),
model: str = typer.Option("model.smt2", "-f"),
workers: int = typer.Option(os.cpu_count(), "-w", help="Number of parallel worker processes"),
model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f"),
):
"""
Pulls out applicable policies, runs who_can
Expand All @@ -79,7 +93,7 @@ def who_can(
if Path(model).is_file():
m.load_model(model)

for x in m.who_can(action, resource, conditions, condition_file, strict_conditions):
for x in m.who_can(action, resource, conditions, condition_file, strict_conditions, workers):
print(x)


Expand Down Expand Up @@ -122,7 +136,8 @@ def who_can_batch_resource(
strict_conditions: bool = typer.Option(
False, help="Whether to require conditions to be passed in for any IAM condition checks"
),
model: str = typer.Option("model.smt2", "-f"),
workers: int = typer.Option(os.cpu_count(), "-w", help="Number of parallel worker processes"),
model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f"),
):
"""
Pulls out applicable policies, runs who_can
Expand All @@ -134,10 +149,24 @@ def who_can_batch_resource(
with open(resources_file) as fs:
resources = fs.read().strip().split("\n")

for source, resource in m.who_can_batch_resource(action, resources, conditions, condition_file, strict_conditions):
for source, resource in m.who_can_batch_resource(action, resources, conditions, condition_file, strict_conditions, workers):
print(f"{source} can perform {action} on {resource}")


@app.command()
def supports_external(
model: str = typer.Option(DEFAULT_MODEL_FILENAME, "-f"),
):
"""
Pulls out resources that allow access from identities not within the model
"""
m = Model()
if Path(model).is_file():
m.load_model(model)

print(m.supports_external(action, resource))


@app.callback()
def main(verbose: int = typer.Option(0, "--verbose", "-v", count=True)):
"""
Expand Down
7 changes: 4 additions & 3 deletions iamspy/datatypes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
import base64
import ipaddress
import string
import z3
from dateutil.parser import parse
from functools import lru_cache

# equivalient to chars in string.ascii_letters + string.digits + string.punctuation
ANY = z3.Range("!", "~")
Expand Down Expand Up @@ -34,6 +34,7 @@ def parse_string(item, string, wildcard=True, case_sensitive=False):
return z3.InRe(item, regex)


@lru_cache(maxsize=2048)
def _regex_parse_string(string, case_sensitive=False):
def intersperse(parts, char):
output = [char] * (len(parts) * 2 - 1)
Expand Down Expand Up @@ -76,9 +77,9 @@ def _arn(data):


def _bool(data):
if "true" == str(data.lower()):
if "true" == str(data).lower():
return z3.BoolVal(True)
elif "false" == str(data.lower()):
elif "false" == str(data).lower():
return z3.BoolVal(False)
else:
raise TypeError(f"Invalid Bool: {data}")
Expand Down
Loading
Loading