aws-inventory-manager 0.13.2__py3-none-any.whl → 0.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aws-inventory-manager might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: aws-inventory-manager
3
- Version: 0.13.2
3
+ Version: 0.16.0
4
4
  Summary: AWS Resource Inventory Management & Delta Tracking CLI tool
5
5
  Author-email: Troy Larson <troy@calvinware.com>
6
6
  License: MIT
@@ -5,7 +5,9 @@ src/aws/credentials.py,sha256=mxj8lQf7TRG5dznQJ37rGQqfB-viHh_5VCt1C4xgqG0,6374
5
5
  src/aws/rate_limiter.py,sha256=b3dV4A9ftUsGgVMOQmv3IVO0Fkym2o1QDHKpFOdSYbg,5464
6
6
  src/cli/__init__.py,sha256=zstPcm000JdpuUuMRuqL2TkC-bnCpm0syHjAyu8ELcY,387
7
7
  src/cli/config.py,sha256=to_hVDxQh0WNANPDQKYlnZzCsFS6_bnrE745wZgRQe4,4286
8
- src/cli/main.py,sha256=7g1AFowwBXZQj0PrShQxq3FlLTRwUp6e7U-CiezMPFY,147763
8
+ src/cli/main.py,sha256=VUF4os0G-ACS4TE9GN61PyAjgKUVkl833_ulL13uFNY,155571
9
+ src/cloudtrail/__init__.py,sha256=lFrH6WLbeJQ5fCfVdyxQrta3Mr_RYQAM4N-IwyazdZI,179
10
+ src/cloudtrail/query.py,sha256=0uq3j9HQL7uHXyZlTB9CJqQ754fbEjwdpCipIhexMz0,13687
9
11
  src/config_service/__init__.py,sha256=fqYfceBP_kPnp8PjYW9D_FqsWqrrOzsQTftBgp7-aTE,585
10
12
  src/config_service/collector.py,sha256=uW6m1feceYCOgANGWvxhbw23etcaqSQpmdIu-DTsWuk,12254
11
13
  src/config_service/detector.py,sha256=BoJC32DXN2f8x8U8RiOmjkGtnrTQrRuO568FHcMt_xU,8531
@@ -20,6 +22,10 @@ src/delta/differ.py,sha256=-7MESqWyO8srPhs9OlsLJjYx57qPwQRh93GsatqnoW8,6692
20
22
  src/delta/formatters.py,sha256=xhzph_iLGLD8yyflYCMTFZqWPmW9tB7hZsVHsgQDrzM,9209
21
23
  src/delta/models.py,sha256=_biiC136e-C2GljlIeW7ql5vC47Ss1M8a-jgw9K9rFg,5103
22
24
  src/delta/reporter.py,sha256=yqzof9snkJyJiOeGoLAla2pmnFNIPlR3brg7xrstpNA,8775
25
+ src/matching/__init__.py,sha256=cidLzXlQo29bV0OXX7AC5A9OLcEStB0NHb61WeatDDo,239
26
+ src/matching/config.py,sha256=eUACdOEUMyEjFzEuKxx4TevcpTIyey555Oq11yV9_6U,1703
27
+ src/matching/normalizer.py,sha256=D_ZNU9U43byVtpLsTF7B6K6CayKjtQ4TKxkSpNzDwTs,15846
28
+ src/matching/prompts.py,sha256=2sqsWktMcygvgOLoHpMeNO1I7_JqprhmGlwzV9S-vz4,1678
23
29
  src/models/__init__.py,sha256=5DFL_8ZgLE28EG8ORumMT3EfhFAxXrFa3yzQpQBSQxc,533
24
30
  src/models/config_diff.py,sha256=elh4oUgoln_sDC6lmkqfSd8k1pX9oqBPqTEgTksw5f8,4237
25
31
  src/models/cost_report.py,sha256=BzyWrMewHau7EpQyLm4WoesVY3WDPBOtGVdk_H8USuM,3079
@@ -57,7 +63,7 @@ src/security/checks/s3_checks.py,sha256=usHrGBmmIZc4PEDSMRRE1H8A_NwUxV2kiIVeLaPC
57
63
  src/security/checks/secrets_checks.py,sha256=2bJY0Hqb1PwUYid5XZCC6y4hgWle3sRtwYNVh4jmUBw,3631
58
64
  src/security/checks/sg_checks.py,sha256=BmipY31beybajxSbS_HoDkSXFYTQFhjXWNxuYYPEP2U,4731
59
65
  src/snapshot/__init__.py,sha256=trHfRe_8jxjQVb48cDLVH3gs2Y7YQtsX2gFL4OB9qRc,237
60
- src/snapshot/capturer.py,sha256=DZqDnyeum9m4AzSH-jj6wrz5I3QpHcdAHWRAE-LL4qc,17975
66
+ src/snapshot/capturer.py,sha256=5Pg3vITpsvr1r0J-wesUb7KnMLsAbxttd0EKsnF3K3Q,18046
61
67
  src/snapshot/filter.py,sha256=UPVTi23NHJYKFTD4ss8PazDHh-NIC2lWLvjJnr9Y7QY,9158
62
68
  src/snapshot/inventory_storage.py,sha256=eOwGzYhUrvnv_usgEaUF-0_cDGGf21k-vjRRKP7Z5Hs,7739
63
69
  src/snapshot/report_formatter.py,sha256=WR7Adqq5FjJqu3iIWAnOZ9yp6AuJH1kA6uKKpT7jrT4,8266
@@ -79,6 +85,7 @@ src/snapshot/resource_collectors/eks.py,sha256=T7Rh4ij3frxYgIfAcZNk0xNpoahwBfsQW
79
85
  src/snapshot/resource_collectors/elasticache_collector.py,sha256=g7mVa9ZlE0DBITnA-i565REZRpumWMnqZuLVHcVO0Gw,2830
80
86
  src/snapshot/resource_collectors/elb.py,sha256=af3IeKUv_FUfcLwYcnDajh4_4eSWtelvoazKCptehnQ,5125
81
87
  src/snapshot/resource_collectors/eventbridge.py,sha256=abHxh0Iob6YrumdzmFugpRGWFqE0uVRVt89gajdq5q4,6062
88
+ src/snapshot/resource_collectors/glue.py,sha256=InEzfjcTDfVeKlM_XATEHKuOimle0QvaNBkbAYmPDto,7779
82
89
  src/snapshot/resource_collectors/iam.py,sha256=N5QQO7E5opV-3CiOuoW_W0x-m_2alOjYs0-Q_loS-rQ,6732
83
90
  src/snapshot/resource_collectors/kms.py,sha256=ioJUO2C7XawrGGnvpNA1pVT0GdHDI4pmbeqpACgf7Dc,4524
84
91
  src/snapshot/resource_collectors/lambda_func.py,sha256=bjlWj_orHlU7vVT91c9ZcEAsXSPB7f4jgUWAMfa3C1w,5289
@@ -95,11 +102,11 @@ src/snapshot/resource_collectors/waf.py,sha256=HUNshQ_WHbIK3nAyNwhb21uHkMC7ywSOk
95
102
  src/storage/__init__.py,sha256=4LrT-bm45zLK6VUyKsx3jyK_L270A48t-w2-0ceb9PM,554
96
103
  src/storage/audit_store.py,sha256=zO7L9o-8N31PsiLJ72wvmodoDrFtFeya05Ebfes8a5Y,14123
97
104
  src/storage/database.py,sha256=j-IJ4LiNs7PgXH0L3FtMlmbId1TaGjNCcYjgn3dt7sQ,9465
98
- src/storage/group_store.py,sha256=6sCqKwFTNHUOin08V1AXlN2scam2khSwf_cgisvD-M0,24832
105
+ src/storage/group_store.py,sha256=RLQK1dBkSbXiFtEw9twpoUFgnWiJWo6MyIMhVQkYoU8,25828
99
106
  src/storage/inventory_store.py,sha256=hRxdL2oqDTvtIuyNiR9IoBFjH1jOe3DV43C8Ybt-aTU,10601
100
- src/storage/resource_store.py,sha256=MB8JVvlq5zZJcjLbIL6R6dtPfqK6hpV0JhzSjiofez0,13111
101
- src/storage/schema.py,sha256=PFDVbC12RGh-VP1CNLqFc6V_QCmblGkf63VCaHuPKGU,10274
102
- src/storage/snapshot_store.py,sha256=TI87NQmpMRG1lXcx8HvcKGPJuOyhzH0WmaUK0C2blZc,11855
107
+ src/storage/resource_store.py,sha256=mfKiVQgok2z_PWi7cJx2VD_aMxL6ioI7TaPpEV5hS40,13220
108
+ src/storage/schema.py,sha256=JvHl5GrSPNyFntH7KDVMVG-_rebo3uxCtVoOUwxFKCc,12275
109
+ src/storage/snapshot_store.py,sha256=6BzGjsIljEetGQ88qXoGP5tsFxPNitT5ZVW2oRoCV_I,12589
103
110
  src/utils/__init__.py,sha256=1qjjL3wxB7ye6s3AT4h5hQ9Xgi7y0WllUMmTpiZWC54,284
104
111
  src/utils/export.py,sha256=lBUxLGkvtdzTFq2VKRmHgf18ZROiorHGGHlbeG7ixxc,8929
105
112
  src/utils/hash.py,sha256=w6jJqNR64IEcgMIrIY-M1QXuo_CkH0sbT8I6JcFQJqo,1747
@@ -134,12 +141,12 @@ src/web/templates/pages/diff.html,sha256=UeRqKd6Op2XXNkNg916km7KICMPngh8FVxiaSQu
134
141
  src/web/templates/pages/error.html,sha256=rCnXvdqhrXQX8RungpL35q6iAU77ntQTnC-B52zYkdo,1715
135
142
  src/web/templates/pages/groups.html,sha256=N2Bj44Bx5hTiXap9b_dIPjan_CS_9pAqJeJYAs5o-z0,43176
136
143
  src/web/templates/pages/queries.html,sha256=sf167CjkCQmx0jZQsEorgSO2N3zVyISdEIpxkhZVr7E,12443
137
- src/web/templates/pages/resources.html,sha256=fygTKEcER7e_L2jDKZD6G3MewWFWBvIHGMbBxW8GqMY,123801
144
+ src/web/templates/pages/resources.html,sha256=hGr2-NDUlk16RWMEkebEMjtAlEpwVvy59KujoZ3_jCk,124172
138
145
  src/web/templates/pages/snapshot_detail.html,sha256=hHoM_smOhVnDhsqHRrjv_Bk3V2WsUcsjOhFEy6qqDDs,12457
139
146
  src/web/templates/pages/snapshots.html,sha256=3b47IQgkN4mxJpqjUaJ_lPqYUqM2Icz44Ad8CesZpvM,22536
140
- aws_inventory_manager-0.13.2.dist-info/LICENSE,sha256=-lY65BqqcGV9QVjIoTpYEB0Jaddm9j-cS5ICDRBRQo0,1071
141
- aws_inventory_manager-0.13.2.dist-info/METADATA,sha256=F4QepgckeuJ9n529xrQrLlBBztr7uzSuNTchDF_sxtw,42531
142
- aws_inventory_manager-0.13.2.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
143
- aws_inventory_manager-0.13.2.dist-info/entry_points.txt,sha256=Ktdhto-PER5BtwWKYBtU_-FS3ngiGtAGGeoLzRW2qUw,44
144
- aws_inventory_manager-0.13.2.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
145
- aws_inventory_manager-0.13.2.dist-info/RECORD,,
147
+ aws_inventory_manager-0.16.0.dist-info/LICENSE,sha256=-lY65BqqcGV9QVjIoTpYEB0Jaddm9j-cS5ICDRBRQo0,1071
148
+ aws_inventory_manager-0.16.0.dist-info/METADATA,sha256=AK-ihmCdQsRVdob6LbCZ9U0CmogO0wmvKChT1WMXWdM,42531
149
+ aws_inventory_manager-0.16.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
150
+ aws_inventory_manager-0.16.0.dist-info/entry_points.txt,sha256=Ktdhto-PER5BtwWKYBtU_-FS3ngiGtAGGeoLzRW2qUw,44
151
+ aws_inventory_manager-0.16.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
152
+ aws_inventory_manager-0.16.0.dist-info/RECORD,,
src/cli/main.py CHANGED
@@ -823,8 +823,11 @@ def snapshot_create(
823
823
  exclude_tags: Optional[str] = typer.Option(
824
824
  None, "--exclude-tags", help="Exclude resources with ANY of these tags (Key=Value,Key2=Value2)"
825
825
  ),
826
+ created_by_role: Optional[str] = typer.Option(
827
+ None, "--created-by-role", help="Tag resources created by this IAM role with _created_by_role (queries CloudTrail, 90-day limit)"
828
+ ),
826
829
  use_config: bool = typer.Option(
827
- True, "--use-config/--no-config", help="Use AWS Config for collection when available (default: enabled)"
830
+ False, "--config", help="Use AWS Config for collection when available (default: disabled, use direct API)"
828
831
  ),
829
832
  config_aggregator: Optional[str] = typer.Option(
830
833
  None, "--config-aggregator", help="AWS Config Aggregator name for multi-account collection"
@@ -835,7 +838,7 @@ def snapshot_create(
835
838
  ):
836
839
  """Create a new snapshot of AWS resources.
837
840
 
838
- Captures resources from 25 AWS services:
841
+ Captures resources from 26 AWS services:
839
842
  - IAM: Roles, Users, Groups, Policies
840
843
  - Lambda: Functions, Layers
841
844
  - S3: Buckets
@@ -860,6 +863,7 @@ def snapshot_create(
860
863
  - CodePipeline: Pipelines
861
864
  - CodeBuild: Projects
862
865
  - Backup: Backup Plans, Backup Vaults
866
+ - Glue: Databases, Tables, Crawlers, Jobs, Connections
863
867
 
864
868
  Historical Baselines & Filtering:
865
869
  Use --before-date, --after-date, --include-tags, and/or --exclude-tags to create
@@ -1041,6 +1045,50 @@ def snapshot_create(
1041
1045
  config_aggregator=config_aggregator,
1042
1046
  )
1043
1047
 
1048
+ # Tag resources created by role if specified (uses CloudTrail)
1049
+ if created_by_role:
1050
+ from ..cloudtrail import CloudTrailQuery
1051
+
1052
+ console.print(f"\n🔍 Checking creator role: [bold]{created_by_role}[/bold]")
1053
+ console.print(" Querying CloudTrail (this may take a moment)...")
1054
+
1055
+ ct_query = CloudTrailQuery(profile_name=aws_profile, regions=region_list)
1056
+ created_resources = ct_query.get_created_resource_names(
1057
+ role_arn=created_by_role,
1058
+ days_back=90,
1059
+ regions=region_list,
1060
+ )
1061
+
1062
+ # Build a set of (name, type) tuples for matching
1063
+ created_set = set()
1064
+ for resource_type, names in created_resources.items():
1065
+ for name in names:
1066
+ created_set.add((name, resource_type))
1067
+
1068
+ # Tag resources that were created by this role (add to tags dict)
1069
+ matched_count = 0
1070
+ for resource in snapshot.resources:
1071
+ is_match = False
1072
+ # Match by name and type
1073
+ if (resource.name, resource.resource_type) in created_set:
1074
+ is_match = True
1075
+ # Also try matching by ARN components
1076
+ elif resource.arn:
1077
+ arn_name = resource.arn.split("/")[-1].split(":")[-1]
1078
+ if (arn_name, resource.resource_type) in created_set:
1079
+ is_match = True
1080
+
1081
+ if is_match:
1082
+ matched_count += 1
1083
+ # Add created-by tag to resource (internal tracking, not AWS tag)
1084
+ if resource.tags is None:
1085
+ resource.tags = {}
1086
+ resource.tags["_created_by_role"] = created_by_role
1087
+
1088
+ console.print(f" Found {len(created_resources)} resource types in CloudTrail")
1089
+ console.print(f" Tagged {matched_count}/{len(snapshot.resources)} resources as created by this role")
1090
+ console.print(f" [dim](Resources older than 90 days won't appear in CloudTrail)[/dim]")
1091
+
1044
1092
  # T018: Check for zero resources after filtering
1045
1093
  if snapshot.resource_count == 0:
1046
1094
  console.print("⚠️ Warning: Snapshot contains 0 resources after filtering", style="bold yellow")
@@ -1147,7 +1195,7 @@ def snapshot_create(
1147
1195
  console.print("\n [dim]Use --verbose to see detailed breakdown by resource type[/dim]")
1148
1196
  elif not use_config:
1149
1197
  console.print("\nCollection Method:")
1150
- console.print(" All resources collected via Direct API (--no-config specified)")
1198
+ console.print(" All resources collected via Direct API (use --config to enable AWS Config)")
1151
1199
 
1152
1200
  except typer.Exit:
1153
1201
  # Re-raise Exit exceptions (normal exit codes)
@@ -3617,6 +3665,157 @@ def serve(
3617
3665
  )
3618
3666
 
3619
3667
 
3668
+ # =============================================================================
3669
+ # Normalize Command (AI Normalization)
3670
+ # =============================================================================
3671
+
3672
+
3673
+ @app.command()
3674
+ def normalize(
3675
+ snapshot: str = typer.Option(..., "--snapshot", "-s", help="Snapshot name to normalize"),
3676
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview normalizations without saving"),
3677
+ use_ai: bool = typer.Option(True, "--ai/--no-ai", help="Use AI for ambiguous names (default: enabled)"),
3678
+ ):
3679
+ """Re-run AI normalization on an existing snapshot.
3680
+
3681
+ This command updates the normalized_name column for all resources
3682
+ in the specified snapshot using AI-based name normalization.
3683
+
3684
+ Use this to:
3685
+ - Backfill normalized names for snapshots created before AI normalization
3686
+ - Re-normalize with updated AI models or prompts
3687
+ - Preview normalizations with --dry-run before committing
3688
+
3689
+ Example:
3690
+ awsinv normalize --snapshot my-snapshot-20260113
3691
+ awsinv normalize --snapshot my-snapshot --dry-run
3692
+ awsinv normalize --snapshot my-snapshot --no-ai
3693
+ """
3694
+ global config
3695
+ if config is None:
3696
+ config = Config.load()
3697
+
3698
+ from ..storage import Database, SnapshotStore
3699
+
3700
+ try:
3701
+ from ..matching import NormalizerConfig, ResourceNormalizer
3702
+ except ImportError:
3703
+ console.print(
3704
+ "[red]AI dependencies not installed.[/red]\n"
3705
+ "Install with: [cyan]pip install aws-inventory-manager[ai][/cyan]"
3706
+ )
3707
+ raise typer.Exit(code=1)
3708
+
3709
+ # Initialize database
3710
+ db = Database(config.storage_path)
3711
+ snapshot_store = SnapshotStore(db)
3712
+
3713
+ # Check snapshot exists
3714
+ if not snapshot_store.exists(snapshot):
3715
+ console.print(f"[red]✗ Snapshot '{snapshot}' not found[/red]")
3716
+ raise typer.Exit(code=1)
3717
+
3718
+ # Load the snapshot
3719
+ console.print(f"[cyan]Loading snapshot '[bold]{snapshot}[/bold]'...[/cyan]")
3720
+ snapshot_obj = snapshot_store.load(snapshot)
3721
+
3722
+ if not snapshot_obj or not snapshot_obj.resources:
3723
+ console.print(f"[yellow]⚠ Snapshot '{snapshot}' has no resources to normalize[/yellow]")
3724
+ raise typer.Exit(code=0)
3725
+
3726
+ console.print(f" Found [bold]{len(snapshot_obj.resources)}[/bold] resources")
3727
+
3728
+ # Initialize normalizer
3729
+ normalizer_config = NormalizerConfig.from_env()
3730
+
3731
+ if use_ai and not normalizer_config.is_ai_enabled:
3732
+ console.print("[yellow]⚠ OPENAI_API_KEY not set - using rules-based normalization only[/yellow]")
3733
+ use_ai = False
3734
+
3735
+ normalizer = ResourceNormalizer(normalizer_config)
3736
+
3737
+ # Prepare resources for normalization
3738
+ resource_dicts = [
3739
+ {
3740
+ "arn": r.arn,
3741
+ "name": r.name,
3742
+ "resource_type": r.resource_type,
3743
+ "tags": r.tags,
3744
+ }
3745
+ for r in snapshot_obj.resources
3746
+ ]
3747
+
3748
+ # Run normalization
3749
+ if use_ai:
3750
+ console.print("[cyan]Running AI-assisted normalization...[/cyan]")
3751
+ else:
3752
+ console.print("[cyan]Running rules-based normalization...[/cyan]")
3753
+
3754
+ with console.status("[bold green]Normalizing resources..."):
3755
+ normalized_names = normalizer.normalize_resources(resource_dicts, use_ai=use_ai)
3756
+
3757
+ console.print(f" Normalized [bold]{len(normalized_names)}[/bold] resource names")
3758
+
3759
+ if normalizer.tokens_used > 0:
3760
+ console.print(f" AI tokens used: [dim]{normalizer.tokens_used}[/dim]")
3761
+
3762
+ # Show preview in dry-run mode
3763
+ if dry_run:
3764
+ console.print("\n[yellow]DRY RUN - No changes saved[/yellow]\n")
3765
+
3766
+ # Build a table of changes
3767
+ table = Table(title="Normalization Preview (first 20)")
3768
+ table.add_column("Resource Type", style="cyan")
3769
+ table.add_column("Original Name", style="white")
3770
+ table.add_column("Normalized Name", style="green")
3771
+
3772
+ count = 0
3773
+ for r in snapshot_obj.resources:
3774
+ if count >= 20:
3775
+ break
3776
+ norm_name = normalized_names.get(r.arn, r.name)
3777
+ # Only show if different or meaningful
3778
+ table.add_row(
3779
+ r.resource_type.split("::")[-1] if r.resource_type else "Unknown",
3780
+ r.name or "(no name)",
3781
+ norm_name,
3782
+ )
3783
+ count += 1
3784
+
3785
+ console.print(table)
3786
+
3787
+ if len(snapshot_obj.resources) > 20:
3788
+ console.print(f"\n[dim]... and {len(snapshot_obj.resources) - 20} more resources[/dim]")
3789
+
3790
+ console.print("\n[yellow]Run without --dry-run to save changes[/yellow]")
3791
+ raise typer.Exit(code=0)
3792
+
3793
+ # Update database with normalized names
3794
+ console.print("[cyan]Updating database...[/cyan]")
3795
+
3796
+ snapshot_id = snapshot_store.get_id(snapshot)
3797
+ if snapshot_id is None:
3798
+ console.print("[red]✗ Failed to get snapshot ID[/red]")
3799
+ raise typer.Exit(code=1)
3800
+
3801
+ updated_count = 0
3802
+ with db.transaction() as cursor:
3803
+ for r in snapshot_obj.resources:
3804
+ norm_name = normalized_names.get(r.arn)
3805
+ if norm_name:
3806
+ cursor.execute(
3807
+ """
3808
+ UPDATE resources
3809
+ SET normalized_name = ?
3810
+ WHERE snapshot_id = ? AND arn = ?
3811
+ """,
3812
+ (norm_name, snapshot_id, r.arn),
3813
+ )
3814
+ updated_count += cursor.rowcount
3815
+
3816
+ console.print(f"[green]✓ Updated {updated_count} resources with normalized names[/green]")
3817
+
3818
+
3620
3819
  def cli_main():
3621
3820
  """Entry point for console script."""
3622
3821
  app()
@@ -0,0 +1,5 @@
1
+ """CloudTrail query module for resource provenance tracking."""
2
+
3
+ from .query import CloudTrailQuery, ResourceCreationEvent
4
+
5
+ __all__ = ["CloudTrailQuery", "ResourceCreationEvent"]