Metadata-Version: 2.4
Name: lambda-preflight
Version: 0.1.0
Summary: Catch AWS Lambda IAM permission errors and event-shape bugs before you deploy — a local pre-flight linter and AWS-faithful runtime. No Docker, no SAM, no invocation charges.
Project-URL: Homepage, https://github.com/swaranshu-borgaonkar/lambda-preflight
Project-URL: Issues, https://github.com/swaranshu-borgaonkar/lambda-preflight/issues
Author: Swar Borgaonkar
License: MIT
License-File: LICENSE
Keywords: aws,iam,lambda,linter,local,mock,preflight,pytest,serverless,testing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.8
Provides-Extra: aws
Requires-Dist: boto3>=1.26; extra == 'aws'
Provides-Extra: dev
Requires-Dist: boto3>=1.26; extra == 'dev'
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# lambda-preflight

**Catch AWS Lambda IAM permission errors and event-shape bugs before you deploy.**

Every "local lambda" tool runs your handler. `lambda-preflight` does something none of them do: it **checks your handler's AWS calls against your execution role's real IAM policies** and fails locally — so you find the missing `s3:PutObject` on your laptop, not in a CloudWatch log after a failed deploy.

No Docker. No SAM CLI. No cloud invocations, so no invocation charges.

```bash
pip install "lambda-preflight[aws]"
```

---

## The headline: local IAM linting

Your Lambda runs as an execution *role*. When that role is missing a permission, you normally find out the slow way — deploy, invoke, read the `AccessDenied` in the logs, redeploy. `lambda-preflight` pulls your role's **real** policies once, caches them, and checks every AWS call your handler makes *before* anything leaves your machine.

```python
from lambda_preflight import pull_role, PolicyEvaluator, intercept_boto3
from lambda_preflight import LambdaHarness, events

# Pull the execution role's real policies (managed + inline + boundary), cached after first call.
policies  = pull_role("my-function-exec-role")
evaluator = PolicyEvaluator(policies, strict=True)

harness = LambdaHarness("app.handler", timeout=30)

# boto3 calls inside the handler are authorized against the real policy — locally.
with intercept_boto3(evaluator):
    result = harness.invoke(events.sqs(body={"orderId": 123}))
    # If app.handler calls s3:PutObject and the role can't → AccessDenied raised here,
    # with the same reason AWS would give, before you ever deploy.
```

Three ways to load policies, so it works on a plane or in locked-down accounts:

```python
from lambda_preflight import pull_role, load_role_policies, PolicySet

pull_role("my-exec-role")                       # 1. live pull  — needs creds + IAM read, caches result
load_role_policies("my-exec-role")              # 2. offline    — reuse cache, no AWS call, no charge
PolicySet.from_policy_documents([{"Statement": [...]}])  # 3. BYO JSON — when you can't pull
```

It models the parts that actually decide real permissions: explicit-deny-wins, implicit deny, wildcard actions, resource matching, permission-boundary intersection, and common condition operators. What it **can't** faithfully evaluate (uncommon condition keys, SCPs, resource-based policies) it **flags** — in `strict=True` mode it raises `CannotEvaluate` rather than returning a false green. It tells you the edge of its own knowledge instead of guessing.

---

## The foundation: an AWS-faithful local runtime

The linter needs something real to run against, so the runtime half is built to match AWS's actual invocation contract — not a stubbed approximation.

### Schema-accurate event fixtures

The #1 cause of "worked locally, broke in AWS" is a test event whose *shape* is wrong. These builders emit payloads matching AWS's real schemas, so what you test is what you'll get. REST (v1) and HTTP (v2) API Gateway differ meaningfully — they're separate on purpose:

```python
events.api_gateway_v1(method="GET", path="/health")
events.api_gateway_v2(method="POST", path="/orders", body={"id": 1})
events.sqs(records=[{"a": 1}, {"b": 2}])        # real batch shape, messageAttributes, md5
events.sns(message={"x": 1}, subject="hi")
events.s3(bucket="b", key="k.txt", event_name="ObjectCreated:Put")
events.eventbridge(detail={"k": "v"}, detail_type="my.type", source="svc")
events.dynamodb_stream(keys={"id": {"S": "1"}}, event_name="INSERT")
```

### Faithful invocation semantics

```python
result = harness.invoke(event)
result.ok          # success vs. error
result.payload     # what the handler returned
result.error       # {errorType, errorMessage, stackTrace} — AWS's exact envelope
result.cold_start  # models warm-container reuse across invokes
```

The `context` reproduces real fields (`aws_request_id`, `invoked_function_arn`, log names) and a live `get_remaining_time_in_millis()` countdown. Timeouts are enforced the way AWS enforces them. Lambda's environment variables are injected so handler code that reads them behaves the same.

### pytest, because CI is the point

Fixtures register automatically — fast, cloud-free, zero-charge tests:

```python
def test_orders(lambda_harness, lambda_events):
    harness = lambda_harness("app.handler", timeout=5)
    result = harness.invoke(lambda_events.sqs(body={"order": 42}))
    assert result.ok
```

---

## How this differs from other local-lambda tools

Tools like `python-lambda-local` and `local-aws-lambda` are **runners** — they execute your handler against an event dict you supply. That's the commodity half, and it's solved. `lambda-preflight` includes a competent runner too, but its reason to exist is what sits on top:

| | Typical local-lambda tools | lambda-preflight |
|---|---|---|
| Run a handler locally | ✅ | ✅ |
| Schema-accurate event fixtures | ✋ you write the dict | ✅ built-in, v1 ≠ v2 |
| Faithful context / timeout / cold-start / AWS-shape errors | partial | ✅ |
| **IAM permission linting against your real role** | ❌ | ✅ **unique** |

---

## Honest scope

This kills the **shape / contract / runtime-semantics / identity-permission** class of "works locally, breaks in AWS" failures — which is most of the day-to-day pain. It does **not**, and a local tool cannot, reproduce cloud-only realities: cold-start latency, live concurrency, account quotas, **SCPs**, and **resource-based policies** (the last two are recorded in `PolicySet.unscoped`, never silently ignored). The IAM layer is a **pre-flight linter, not a parity guarantee**. For final confidence, run against real AWS credentials.

## License

MIT
