Skip to content
Open
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
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ ast-grep-core = "0.39"
schemars = { version = "^1", features = ["derive"] }
rust-embed = { version = "8.9", features = ["compression", "include-exclude"] }
reqwest = { version = "0.12.4", features = ["rustls-tls"], default-features = false }
openssl = { version = "0.10", features = ["vendored"] }

# Native async runtime and parallel processing
tokio = { version = "1.0", features = ["fs", "rt", "rt-multi-thread", "macros", "signal"] }
Expand All @@ -46,7 +47,7 @@ criterion = "0.5"
proptest = "1.0"

# CLI-specific dependencies
clap = { version = "4.5", features = ["derive", "env"] }
clap = { version = "4.5", features = ["derive", "env", "cargo"] }
env_logger = "0.11"
log = "0.4"
walkdir = "2.0"
Expand All @@ -64,7 +65,9 @@ serial_test = "3.0"
atty = "0.2"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.8", features = ["v4"] }
sha2 = "0.10"
aws-lc-rs = "1.15.2"
git2 = "0.20.3"
relative-path = "2.0.1"
url = "2.5"
percent-encoding = "2.3"
aws-sdk-iam = "1.89.0"
Expand Down
27 changes: 26 additions & 1 deletion iam-policy-autopilot-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
//! all denial types with appropriate branching logic.
use crate::{output, types::ExitCode};
use clap::crate_version;
use iam_policy_autopilot_access_denied::{ApplyError, ApplyOptions, DenialType};

fn is_tty() -> bool {
atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stderr)
}
Expand Down Expand Up @@ -143,6 +143,31 @@ async fn fix_access_denied_with_service(
}
}

pub fn print_version_info(verbose: bool) -> anyhow::Result<()> {
println!("{}", crate_version!());
if verbose {
let boto3_version_metadata =
iam_policy_autopilot_policy_generation::api::get_boto3_version_info()?;
let botocore_version_metadata =
iam_policy_autopilot_policy_generation::api::get_botocore_version_info()?;
println!(
"boto3 version: commit_id={}, commit_tag={}, data_hash={}",
boto3_version_metadata.git_commit_hash,
boto3_version_metadata.git_tag.unwrap_or("None".to_string()),
boto3_version_metadata.data_hash
);
println!(
"botocore version: commit_id={}, commit_tag={}, data_hash={}",
botocore_version_metadata.git_commit_hash,
botocore_version_metadata
.git_tag
.unwrap_or("None".to_string()),
botocore_version_metadata.data_hash
);
}
Ok(())
}

fn handle_apply_error(apply_error: ApplyError) -> ExitCode {
match apply_error {
ApplyError::UnsupportedDenialType => {
Expand Down
21 changes: 21 additions & 0 deletions iam-policy-autopilot-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ mod types;
use iam_policy_autopilot_mcp_server::{start_mcp_server, McpTransport};
use types::ExitCode;

use crate::commands::print_version_info;

/// Default port for mcp server for Http Transport
static MCP_HTTP_DEFAULT_PORT: u16 = 8001;

Expand Down Expand Up @@ -112,6 +114,7 @@ required for the operations you perform (e.g., KMS actions for S3 encryption).";
name = "iam-policy-autopilot",
author,
version,
disable_version_flag = true,
about = "Generate IAM policies from source code and fix AccessDenied errors",
long_about = "Unified tool that combines IAM policy generation from source code analysis \
with automatic AccessDenied error fixing. Supports three main operations:\n\n\
Expand Down Expand Up @@ -349,6 +352,16 @@ for direct integration with IDEs and tools. 'http' starts an HTTP server for net
Only used when --transport=http. The server will bind to 127.0.0.1 (localhost) on the specified port.")]
port: u16,
},

#[command(
about = "Print version information.",
short_flag = 'V',
long_flag = "version"
)]
Version {
#[arg(long = "verbose", default_value_t = false, hide = true)]
verbose: bool,
},
}

/// Initialize logging based on configuration
Expand Down Expand Up @@ -611,6 +624,14 @@ async fn main() {
}
}
}

Commands::Version { verbose } => match print_version_info(verbose) {
Ok(()) => ExitCode::Success,
Err(e) => {
print_cli_command_error(e);
ExitCode::Error
}
},
};

process::exit(code.into());
Expand Down
5 changes: 5 additions & 0 deletions iam-policy-autopilot-policy-generation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tokio.workspace = true
async-trait.workspace = true
strsim.workspace = true


# Build dependencies
[build-dependencies]
serde.workspace = true
Expand All @@ -40,6 +41,10 @@ tokio-util.workspace = true
# JSON processing
serde_json.workspace = true

aws-lc-rs.workspace = true
git2.workspace = true
relative-path.workspace = true

[features]
default = []
integ-test = []
Expand Down
138 changes: 129 additions & 9 deletions iam-policy-autopilot-policy-generation/build.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
use aws_lc_rs::digest::{Context, Digest, SHA256};
use git2::{DescribeOptions, Repository};
use relative_path::PathExt;
use relative_path::RelativePathBuf;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;

/// Simplified service definition with fields removed
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SimplifiedServiceDefinition {
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<String>,
metadata: ServiceMetadata,
operations: HashMap<String, SimplifiedOperation>,
shapes: HashMap<String, SimplifiedShape>,
operations: BTreeMap<String, SimplifiedOperation>,
shapes: BTreeMap<String, SimplifiedShape>,
}

/// Service metadata from AWS service definitions
Expand All @@ -37,8 +43,8 @@ struct SimplifiedOperation {
struct SimplifiedShape {
#[serde(rename = "type")]
type_name: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
members: HashMap<String, ShapeReference>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
members: BTreeMap<String, ShapeReference>,
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<Vec<String>>,
}
Expand All @@ -49,6 +55,60 @@ struct ShapeReference {
shape: String,
}

include!("src/shared_submodule_model.rs");

impl GitSubmoduleMetadata {
fn new(git_path: &Path, data_path: &PathBuf) -> GitSubmoduleMetadata {
let repository = Repository::open(git_path)
.unwrap_or_else(|_| panic!("Failed to open repository at path {:?}", git_path));
GitSubmoduleMetadata {
git_commit_hash: get_repository_commit(&repository).unwrap_or_else(|_| {
panic!("Failed to get repository commit at path {:?}", git_path)
}),
git_tag: get_repository_tag(&repository)
.unwrap_or_else(|_| panic!("Failed to get repository tag at path {:?}", git_path)),
data_hash: format!(
"{:?}",
Self::sha2sum_recursive(data_path, data_path).unwrap_or_else(|_| panic!(
"Failed to compute checksum over data at path {:?}",
data_path
))
),
}
}

/// Recursively computes a deterministic SHA-256 hash of a directory tree. Hashes each file's contents
/// and subdirectory recursively, then combines all hashes with their relative paths in sorted order
/// to produce a single hash representing the entire directory structure and contents.
fn sha2sum_recursive(cwd: &Path, root: &Path) -> Result<Digest, Box<dyn std::error::Error>> {
let mut hash_table: BTreeMap<RelativePathBuf, Digest> = BTreeMap::new();

let mut dir_entry_list = fs::read_dir(cwd)?
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()?;
dir_entry_list.sort();

for entry_path in dir_entry_list {
let relt_path = entry_path.clone().relative_to(root)?;
if entry_path.is_dir() {
hash_table.insert(relt_path, Self::sha2sum_recursive(&entry_path, root)?);
} else {
let mut sha2_context = Context::new(&SHA256);
sha2_context.update(&fs::read(entry_path)?);
hash_table.insert(relt_path, sha2_context.finish());
}
}

let mut sha2_context = Context::new(&SHA256);
for (path, digest) in hash_table {
sha2_context.update(path.into_string().as_bytes());
sha2_context.update(digest.as_ref());
}

Ok(sha2_context.finish())
}
}

fn main() {
println!("cargo:rerun-if-changed=resources/config/sdks/botocore-data");
println!("cargo:rerun-if-changed=resources/config/sdks/boto3");
Expand Down Expand Up @@ -123,6 +183,40 @@ fn main() {
// Copy the boto3 directory to the workspace location
copy_dir_recursive(&boto3_dir, workspace_boto3_embed_dir)
.expect("Failed to copy boto3 simplified data");

let workspace_submodule_version_embed_dir = PathBuf::from("target/submodule-version-info");

// Remove existing directory if it exists
if workspace_submodule_version_embed_dir.exists() {
fs::remove_dir_all(&workspace_submodule_version_embed_dir)
.expect("Failed to remove existing submodule version directory");
}
fs::create_dir_all(&workspace_submodule_version_embed_dir)
.expect("Failed to create submodule version directory");

let boto3_info =
GitSubmoduleMetadata::new(Path::new("resources/config/sdks/boto3"), &boto3_dir);

let boto3_info_json =
serde_json::to_string(&boto3_info).expect("Failed to serialize boto3 version metadata");
fs::write(
workspace_submodule_version_embed_dir.join("boto3_version.json"),
boto3_info_json,
)
.expect("Failed to write boto3 version metadata");

let botocore_info = GitSubmoduleMetadata::new(
Path::new("resources/config/sdks/botocore-data"),
&simplified_dir,
);

let botocore_info_json = serde_json::to_string(&botocore_info)
.expect("Failed to serialize botocore version metadata");
fs::write(
workspace_submodule_version_embed_dir.join("botocore_version.json"),
botocore_info_json,
)
.expect("Failed to write botocore version metadata");
}

fn process_botocore_data(
Expand Down Expand Up @@ -297,8 +391,8 @@ fn extract_metadata(

fn simplify_operations(
operations_value: Option<&Value>,
) -> Result<HashMap<String, SimplifiedOperation>, Box<dyn std::error::Error>> {
let mut simplified_operations = HashMap::new();
) -> Result<BTreeMap<String, SimplifiedOperation>, Box<dyn std::error::Error>> {
let mut simplified_operations = BTreeMap::new();

if let Some(Value::Object(operations)) = operations_value {
for (op_name, op_value) in operations {
Expand All @@ -313,8 +407,8 @@ fn simplify_operations(

fn simplify_shapes(
shapes_value: Option<&Value>,
) -> Result<HashMap<String, SimplifiedShape>, Box<dyn std::error::Error>> {
let mut simplified_shapes = HashMap::new();
) -> Result<BTreeMap<String, SimplifiedShape>, Box<dyn std::error::Error>> {
let mut simplified_shapes = BTreeMap::new();

if let Some(Value::Object(shapes)) = shapes_value {
for (shape_name, shape_value) in shapes {
Expand Down Expand Up @@ -417,3 +511,29 @@ fn process_boto3_service_version(

Ok(has_resources_file)
}

/// Performs git describe --exact-match --tags
fn get_repository_tag(repo: &Repository) -> Result<Option<String>, Box<dyn std::error::Error>> {
let mut describe_options = DescribeOptions::new();
describe_options.max_candidates_tags(0);
describe_options.describe_tags();

Ok(repo
.describe(&describe_options)
.map(|desc| {
Option::Some(
desc.format(Option::None)
.expect("Failed to format describe result"),
)
})
.unwrap_or_default())
}

fn get_repository_commit(repo: &Repository) -> Result<String, Box<dyn std::error::Error>> {
Ok(repo
.revparse_single("HEAD")?
.into_commit()
.expect("Failed to get HEAD commit hash")
.id()
.to_string())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use crate::errors::Result;
use crate::{api::model::GitSubmoduleMetadata, embedded_data::GitSubmoduleVersionInfo};

/// Gets the version information for the boto3 submodule.
///
/// # Returns
///
/// Returns the Git submodule metadata for boto3, including commit hash and version information.
///
/// # Errors
///
/// Returns an error if the boto3 version information cannot be retrieved.
pub fn get_boto3_version_info() -> Result<GitSubmoduleMetadata> {
GitSubmoduleVersionInfo::get_boto3_version_info()
}

/// Gets the version information for the botocore submodule.
///
/// # Returns
///
/// Returns the Git submodule metadata for botocore, including commit hash and version information.
///
/// # Errors
///
/// Returns an error if the botocore version information cannot be retrieved.
pub fn get_botocore_version_info() -> Result<GitSubmoduleMetadata> {
GitSubmoduleVersionInfo::get_botocore_version_info()
}
2 changes: 2 additions & 0 deletions iam-policy-autopilot-policy-generation/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
mod extract_sdk_calls;
mod generate_policies;
mod get_submodule_version;
pub use extract_sdk_calls::extract_sdk_calls;
pub use generate_policies::generate_policies;
pub use get_submodule_version::{get_boto3_version_info, get_botocore_version_info};
mod common;
pub mod model;
4 changes: 4 additions & 0 deletions iam-policy-autopilot-policy-generation/src/api/model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Defined model for API
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// Configuration for generate_policies Api
#[derive(Debug, Clone)]
pub struct GeneratePolicyConfig {
Expand Down Expand Up @@ -49,6 +51,8 @@ pub struct AwsContext {
pub account: String,
}

include!("../shared_submodule_model.rs");

impl AwsContext {
/// Creates a new AwsContext with the partition automatically derived from the region.
///
Expand Down
Loading