Skip to content

Commit 65ac73b

Browse files
committed
testing new version
1 parent 8595b6c commit 65ac73b

3 files changed

Lines changed: 56 additions & 83 deletions

File tree

devolv/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "0.2.21"
1+
__version__ = "0.2.22"
22

devolv/drift/cli.py

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,24 @@
11
import json
2-
import boto3
3-
import typer
42
import os
53
import subprocess
4+
import boto3
5+
import typer
6+
from github import Github
67

78
from devolv.drift.aws_fetcher import get_aws_policy_document, merge_policy_documents, build_superset_policy
89
from devolv.drift.issues import create_approval_issue, wait_for_sync_choice
910
from devolv.drift.github_approvals import create_github_pr
10-
from devolv.drift.report import detect_and_print_drift
11-
from github import Github # Needed for auto-close
1211

1312
app = typer.Typer()
1413

1514
def push_branch(branch_name: str):
16-
import subprocess
17-
import typer
18-
1915
try:
20-
# Create or switch to branch safely
2116
subprocess.run(["git", "checkout", "-B", branch_name], check=True)
22-
23-
# Ensure Git identity is set
2417
subprocess.run(["git", "config", "user.email", "github-actions@users.noreply.github.com"], check=True)
2518
subprocess.run(["git", "config", "user.name", "github-actions"], check=True)
26-
27-
# Add, commit
2819
subprocess.run(["git", "add", "."], check=True)
2920
subprocess.run(["git", "commit", "-m", f"Update policy: {branch_name}"], check=True)
3021

31-
# Try pushing
3222
try:
3323
subprocess.run(["git", "push", "--set-upstream", "origin", branch_name], check=True)
3424
except subprocess.CalledProcessError:
@@ -42,6 +32,22 @@ def push_branch(branch_name: str):
4232
typer.echo(f"❌ Git command failed: {e}")
4333
raise typer.Exit(1)
4434

35+
def detect_drift(local_doc, aws_doc) -> bool:
36+
"""Detect if AWS permissions exist that are missing in local (removal drift)."""
37+
local_statements = {json.dumps(stmt, sort_keys=True) for stmt in local_doc.get("Statement", [])}
38+
aws_statements = {json.dumps(stmt, sort_keys=True) for stmt in aws_doc.get("Statement", [])}
39+
40+
missing_in_local = aws_statements - local_statements
41+
42+
if missing_in_local:
43+
typer.echo("❌ Drift detected: Your local policy is missing some permissions present in AWS.")
44+
for stmt in missing_in_local:
45+
typer.echo(stmt)
46+
return True
47+
48+
typer.echo("✅ No drift detected (local may have extra permissions; that's fine).")
49+
return False
50+
4551
@app.command()
4652
def drift(
4753
policy_name: str = typer.Option(..., "--policy-name", help="Name of the IAM policy"),
@@ -63,25 +69,25 @@ def drift(
6369
raise typer.Exit(1)
6470

6571
aws_doc = get_aws_policy_document(policy_arn)
66-
drift = detect_and_print_drift(local_doc, aws_doc)
6772

68-
if not drift and not approval_anyway:
69-
typer.echo("✅ No drift detected. Use --approval-anyway to force approval.")
73+
drift_detected = detect_drift(local_doc, aws_doc)
74+
75+
if not drift_detected and not approval_anyway:
76+
typer.echo("✅ No drift and no forced approval requested.")
7077
raise typer.Exit()
7178

7279
repo_full_name = repo_full_name or os.getenv("GITHUB_REPOSITORY")
80+
token = os.getenv("GITHUB_TOKEN")
81+
7382
if not repo_full_name:
7483
typer.echo("❌ GitHub repo not specified. Use --repo or set GITHUB_REPOSITORY.")
7584
raise typer.Exit(1)
76-
77-
token = os.getenv("GITHUB_TOKEN")
7885
if not token:
7986
typer.echo("❌ GITHUB_TOKEN not set in environment.")
8087
raise typer.Exit(1)
8188

8289
assignees = [a.strip() for a in approvers.split(",") if a.strip()]
8390
issue_num, _ = create_approval_issue(repo_full_name, token, policy_name, assignees=assignees)
84-
typer.echo(f"✅ Created issue #{issue_num} in {repo_full_name}: https://github.com/{repo_full_name}/issues/{issue_num}")
8591

8692
choice = wait_for_sync_choice(repo_full_name, issue_num, token)
8793
iam = boto3.client("iam")
@@ -115,15 +121,10 @@ def _update_aws_policy(iam, policy_arn, policy_doc):
115121
)
116122

117123
def _update_local_and_create_pr(doc, policy_file, repo_full_name, policy_name, issue_num, token, description=""):
118-
import json
119-
from github import Github
120-
from devolv.drift.github_approvals import create_github_pr
121-
122124
new_content = json.dumps(doc, indent=2)
123125
with open(policy_file, "w") as f:
124126
f.write(new_content)
125127

126-
# Clean branch name
127128
branch = (
128129
f"{description.replace(' ', '-').replace('+', 'plus').replace('/', '-')}-policy-{policy_name}"
129130
.strip("-")
@@ -135,16 +136,10 @@ def _update_local_and_create_pr(doc, policy_file, repo_full_name, policy_name, i
135136
pr_title = f"Update {policy_file} {description}".strip()
136137
pr_body = f"This PR updates `{policy_file}` {description}.\n\nLinked to issue #{issue_num}.".strip()
137138

138-
# ✅ Pass correct branch name
139139
pr_num, pr_url = create_github_pr(repo_full_name, branch, pr_title, pr_body, issue_num=issue_num)
140140

141-
#typer.echo(f"✅ Created PR #{pr_num}: {pr_url}")
142-
143-
# ✅ Auto-close issue immediately
144141
gh = Github(token)
145142
repo = gh.get_repo(repo_full_name)
146143
issue = repo.get_issue(number=issue_num)
147144
issue.create_comment(f"✅ PR created and linked: {pr_url}. Closing issue.")
148145
issue.edit(state="closed")
149-
150-

devolv/drift/report.py

Lines changed: 30 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,80 +13,58 @@ def clean_policy(policy):
1313
policy["Statement"] = [s for s in statements if s]
1414
return policy
1515

16-
def detect_and_print_drift(local_doc: dict, aws_doc: dict) -> bool:
16+
def detect_drift(local_doc: dict, aws_doc: dict) -> bool:
1717
"""
18-
Compare local and AWS policy documents.
19-
Print a rich diff if drift is detected.
20-
Return True if drift is detected, False otherwise.
18+
Detect if the local policy would remove permissions from the AWS policy.
19+
Returns True if drift is detected, False otherwise.
2120
"""
22-
console = Console()
23-
24-
# Clean
2521
local_doc = clean_policy(local_doc)
2622
aws_doc = clean_policy(aws_doc)
2723

28-
# Dump sorted JSON
29-
local_str = json.dumps(local_doc, indent=2, sort_keys=True)
30-
aws_str = json.dumps(aws_doc, indent=2, sort_keys=True)
24+
local_statements = local_doc.get("Statement", [])
25+
aws_statements = aws_doc.get("Statement", [])
26+
27+
# Check if any AWS statement is missing in local (i.e., local would remove something)
28+
missing_in_local = [stmt for stmt in aws_statements if stmt not in local_statements]
29+
30+
return bool(missing_in_local)
3131

32-
# Diff lines
33-
local_lines = local_str.splitlines()
34-
aws_lines = aws_str.splitlines()
32+
def generate_diff_lines(local_doc: dict, aws_doc: dict):
33+
"""
34+
Generate a unified diff between local and AWS policy JSONs.
35+
"""
36+
local_str = json.dumps(clean_policy(local_doc), indent=2, sort_keys=True)
37+
aws_str = json.dumps(clean_policy(aws_doc), indent=2, sort_keys=True)
3538

36-
diff_lines = list(difflib.unified_diff(
37-
local_lines,
38-
aws_lines,
39+
return list(difflib.unified_diff(
40+
local_str.splitlines(),
41+
aws_str.splitlines(),
3942
fromfile="local",
4043
tofile="aws",
4144
lineterm=""
4245
))
4346

47+
def print_drift_diff(local_doc: dict, aws_doc: dict):
48+
"""
49+
Pretty-print a unified diff using Rich.
50+
"""
51+
console = Console()
52+
diff_lines = generate_diff_lines(local_doc, aws_doc)
53+
4454
if not diff_lines:
4555
console.print("✅ No drift detected: Policies match.", style="green")
46-
return False
56+
return
4757

4858
console.print("❌ Drift detected — see diff below", style="bold red")
49-
i = 0
50-
while i < len(diff_lines):
51-
line = diff_lines[i]
5259

60+
for line in diff_lines:
5361
if line.startswith('---') or line.startswith('+++'):
5462
console.print(Text(line, style="bold"))
5563
elif line.startswith('@@'):
5664
console.print(Text(line, style="cyan"))
5765
elif line.startswith('-'):
58-
if (i + 1 < len(diff_lines)) and diff_lines[i + 1].startswith('+'):
59-
next_line = diff_lines[i + 1]
60-
old_content = line[1:].rstrip('\n')
61-
new_content = next_line[1:].rstrip('\n')
62-
63-
matcher = difflib.SequenceMatcher(None, old_content, new_content)
64-
old_text = Text("-", style="red")
65-
new_text = Text("+", style="green")
66-
67-
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
68-
if tag == 'equal':
69-
old_text.append(old_content[i1:i2], style="red")
70-
new_text.append(new_content[j1:j2], style="green")
71-
elif tag == 'replace':
72-
old_text.append(old_content[i1:i2], style="bold white on red")
73-
new_text.append(new_content[j1:j2], style="bold black on green")
74-
elif tag == 'delete':
75-
old_text.append(old_content[i1:i2], style="bold white on red")
76-
elif tag == 'insert':
77-
new_text.append(new_content[j1:j2], style="bold black on green")
78-
79-
console.print(old_text)
80-
console.print(new_text)
81-
i += 1
82-
else:
83-
console.print(Text(line, style="red"))
66+
console.print(Text(line, style="red"))
8467
elif line.startswith('+'):
8568
console.print(Text(line, style="green"))
86-
elif line.startswith(' '):
87-
console.print(Text(line, style="bright_black"))
8869
else:
89-
console.print(Text(line))
90-
i += 1
91-
92-
return True
70+
console.print(Text(line, style="bright_black"))

0 commit comments

Comments
 (0)