Skip to main content
Clanker provides a comprehensive IAM security agent that analyzes AWS Identity and Access Management configurations, identifies security vulnerabilities, and generates automated remediation plans. The IAM integration includes security analysis, policy review, and credential auditing capabilities.

Architecture overview

The IAM agent consists of three main components:
  1. IAM Client (/internal/iam/client.go) - AWS SDK wrapper for IAM operations
  2. Analyzer SubAgent (/internal/iam/analyzer/) - Security finding detection
  3. Fixer SubAgent (/internal/iam/fixer/) - Automated remediation planning
type Agent struct {
    client       *Client
    analyzer     *analyzer.SubAgent
    fixer        *fixer.SubAgent
    conversation *ConversationHistory
    debug        bool
}

Configuration

The IAM agent uses AWS profiles and supports multiple accounts:
aws:
  default_profile: "default"
  profiles:
    - name: "production"
      region: "us-east-1"
    - name: "development"
      region: "us-west-2"

Client initialization

Create an IAM agent with profile and region:
func NewAgentWithOptions(opts AgentOptions) (*Agent, error) {
    client, err := NewClient(opts.Profile, opts.Region, opts.Debug)
    if err != nil {
        return nil, fmt.Errorf("failed to create IAM client: %w", err)
    }
    
    accountID := client.GetAccountID()
    conversation := NewConversationHistory(accountID)
    
    analyzerClient := &analyzerClientAdapter{client: client}
    fixerClient := &fixerClientAdapter{client: client}
    
    return &Agent{
        client:       client,
        analyzer:     analyzer.NewSubAgent(analyzerClient, opts.Debug),
        fixer:        fixer.NewSubAgent(fixerClient, opts.Debug),
        conversation: conversation,
        debug:        opts.Debug,
    }, nil
}
The client automatically retrieves the AWS account ID:
func NewClient(profile, region string, debug bool) (*Client, error) {
    opts := []func(*config.LoadOptions) error{}
    if profile != "" {
        opts = append(opts, config.WithSharedConfigProfile(profile))
    }
    if region != "" {
        opts = append(opts, config.WithRegion(region))
    }
    
    cfg, err := config.LoadDefaultConfig(ctx, opts...)
    if err != nil {
        return nil, fmt.Errorf("failed to load AWS config: %w", err)
    }
    
    client := &Client{
        iam:     iam.NewFromConfig(cfg),
        sts:     sts.NewFromConfig(cfg),
        profile: profile,
        region:  region,
    }
    
    // Get account ID
    callerIdentity, err := client.sts.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
    if err == nil && callerIdentity.Account != nil {
        client.accountID = *callerIdentity.Account
    }
    
    return client, nil
}

Security analysis

Analyze account

Perform comprehensive security analysis across all IAM resources:
clanker ask --iam "analyze my AWS account for security issues"
The analyzer performs multiple checks:
func (a *SubAgent) AnalyzeAccount(ctx context.Context) ([]SecurityFinding, error) {
    var findings []SecurityFinding
    
    // Analyze all roles
    roleFindings, err := a.analyzeAllRoles(ctx)
    findings = append(findings, roleFindings...)
    
    // Analyze all policies
    policyFindings, err := a.analyzeAllPolicies(ctx)
    findings = append(findings, policyFindings...)
    
    // Analyze credential report
    credentialFindings, err := a.analyzeCredentials(ctx)
    findings = append(findings, credentialFindings...)
    
    return findings, nil
}

Analyze specific role

Focus analysis on a single IAM role:
clanker ask --iam --role-arn "arn:aws:iam::123456789012:role/MyRole" "analyze this role"
Implementation:
func (a *SubAgent) AnalyzeRole(ctx context.Context, roleName string) ([]SecurityFinding, error) {
    var findings []SecurityFinding
    
    detail, err := a.client.GetRoleDetails(ctx, roleName)
    if err != nil {
        return nil, fmt.Errorf("failed to get role details: %w", err)
    }
    
    // Analyze trust policy
    trustFindings := AnalyzeTrustPolicy(detail.RoleName, detail.AssumeRolePolicyDocument)
    findings = append(findings, trustFindings...)
    
    // Analyze attached policies
    for _, policy := range detail.AttachedPolicies {
        policyDetail, err := a.client.GetPolicyDocument(ctx, policy.PolicyARN)
        if err != nil {
            continue
        }
        policyFindings := AnalyzePermissions(policy.PolicyARN, policyDetail.PolicyDocument)
        findings = append(findings, policyFindings...)
    }
    
    // Analyze inline policies
    for _, policy := range detail.InlinePolicies {
        policyFindings := AnalyzePermissions(
            fmt.Sprintf("%s (inline: %s)", detail.RoleARN, policy.PolicyName),
            policy.PolicyDocument,
        )
        findings = append(findings, policyFindings...)
    }
    
    // Check if role is unused
    if detail.LastUsed == nil {
        findings = append(findings, SecurityFinding{
            ID:          GenerateFindingID(),
            Severity:    SeverityLow,
            Type:        FindingUnusedRole,
            ResourceARN: detail.RoleARN,
            Description: fmt.Sprintf("Role %s has never been used", detail.RoleName),
            Remediation: "Consider deleting unused roles to reduce attack surface",
        })
    }
    
    return findings, nil
}

Analyze specific policy

clanker ask --iam --policy-arn "arn:aws:iam::123456789012:policy/MyPolicy" "check for security issues"

Security finding types

The analyzer detects multiple security issue types:
const (
    FindingOverpermissivePolicy   = "overpermissive_policy"
    FindingAdminAccess            = "admin_access"
    FindingWildcardResource       = "wildcard_resource"
    FindingUnusedRole             = "unused_role"
    FindingCrossAccountTrust      = "cross_account_trust"
    FindingMissingMFA             = "missing_mfa"
    FindingOldAccessKeys          = "old_access_keys"
    FindingInactiveKeys           = "inactive_keys"
    FindingRootAccountUsage       = "root_account_usage"
    FindingPublicS3Access         = "public_s3_access"
    FindingExcessivePermissions   = "excessive_permissions"
    FindingMissingResourceScoping = "missing_resource_scoping"
)

Severity levels

const (
    SeverityCritical = "critical"
    SeverityHigh     = "high"
    SeverityMedium   = "medium"
    SeverityLow      = "low"
    SeverityInfo     = "info"
)

Security finding structure

type SecurityFinding struct {
    ID          string   `json:"id"`
    Severity    string   `json:"severity"`
    Type        string   `json:"type"`
    ResourceARN string   `json:"resource_arn"`
    Description string   `json:"description"`
    Remediation string   `json:"remediation"`
    Actions     []string `json:"actions,omitempty"`
    Resources   []string `json:"resources,omitempty"`
}

Automated remediation

Generate fix plan

Request automated remediation for security findings:
clanker ask --iam "fix overpermissive IAM policies"
The fixer generates a detailed remediation plan:
func (f *SubAgent) GenerateFixPlan(ctx context.Context, finding SecurityFinding) (*FixPlan, error) {
    plan := &FixPlan{
        ID:        generatePlanID(),
        Finding:   finding,
        CreatedAt: time.Now(),
    }
    
    switch finding.Type {
    case FindingOverpermissivePolicy:
        commands, notes, warnings := f.planOverpermissivePolicyFix(ctx, finding)
        plan.Commands = commands
        plan.Notes = notes
        plan.Warnings = warnings
        plan.Summary = "Restrict overly permissive IAM policy"
        
    case FindingAdminAccess:
        commands, notes, warnings := f.planAdminAccessFix(ctx, finding)
        plan.Commands = commands
        plan.Notes = notes
        plan.Warnings = warnings
        plan.Summary = "Review and restrict administrative IAM access"
        
    case FindingWildcardResource:
        commands, notes, warnings := f.planWildcardResourceFix(ctx, finding)
        plan.Commands = commands
        plan.Notes = notes
        plan.Warnings = warnings
        plan.Summary = "Add resource scoping to IAM policy"
        
    // ... additional finding types
    }
    
    return plan, nil
}

Fix plan structure

type FixPlan struct {
    ID        string          `json:"id"`
    Summary   string          `json:"summary"`
    Finding   SecurityFinding `json:"finding"`
    Commands  []FixCommand    `json:"commands"`
    Notes     []string        `json:"notes,omitempty"`
    Warnings  []string        `json:"warnings,omitempty"`
    CreatedAt time.Time       `json:"created_at"`
}

type FixCommand struct {
    ID          string                 `json:"id"`
    Action      string                 `json:"action"`
    ResourceARN string                 `json:"resource_arn"`
    Parameters  map[string]interface{} `json:"parameters"`
    Reason      string                 `json:"reason"`
    Rollback    *FixCommand            `json:"rollback,omitempty"`
}

Apply fix plan

Execute automated remediation (requires confirmation):
func (f *SubAgent) ApplyPlan(ctx context.Context, plan *FixPlan, confirm bool) error {
    if err := f.ValidatePlan(plan); err != nil {
        return fmt.Errorf("invalid plan: %w", err)
    }
    
    if !confirm {
        return fmt.Errorf("plan execution requires confirmation")
    }
    
    for i, cmd := range plan.Commands {
        if err := f.executeCommand(ctx, cmd); err != nil {
            return fmt.Errorf("command %d (%s) failed: %w", i+1, cmd.Action, err)
        }
    }
    
    return nil
}

Supported fix actions

const (
    ActionUpdatePolicy        = "update_policy"
    ActionCreatePolicyVersion = "create_policy_version"
    ActionAttachPolicy        = "attach_policy"
    ActionDetachPolicy        = "detach_policy"
    ActionDeletePolicyVersion = "delete_policy_version"
    ActionDeactivateAccessKey = "deactivate_access_key"
    ActionDeleteAccessKey     = "delete_access_key"
    ActionRotateAccessKey     = "rotate_access_key"
    ActionUpdateTrustPolicy   = "update_trust_policy"
)

Credential analysis

The IAM agent generates and analyzes AWS credential reports:
func (c *Client) GetCredentialReport(ctx context.Context) (*CredentialReport, error) {
    // Generate credential report
    for i := 0; i < 10; i++ {
        _, err := c.iam.GenerateCredentialReport(ctx, &iam.GenerateCredentialReportInput{})
        if err == nil {
            break
        }
        time.Sleep(time.Second * 2)
    }
    
    // Get the report
    resp, err := c.iam.GetCredentialReport(ctx, &iam.GetCredentialReportInput{})
    if err != nil {
        return nil, fmt.Errorf("failed to get credential report: %w", err)
    }
    
    report := &CredentialReport{}
    if resp.GeneratedTime != nil {
        report.GeneratedTime = *resp.GeneratedTime
    }
    
    // Parse CSV content
    reader := csv.NewReader(strings.NewReader(string(resp.Content)))
    records, err := reader.ReadAll()
    if err != nil {
        return nil, fmt.Errorf("failed to parse credential report: %w", err)
    }
    
    // Parse entries and check for:
    // - Missing MFA
    // - Old access keys
    // - Inactive keys
    // - Unused passwords
    
    return report, nil
}
Credential report entry:
type CredentialReportEntry struct {
    User                      string     `json:"user"`
    ARN                       string     `json:"arn"`
    UserCreationTime          time.Time  `json:"user_creation_time"`
    PasswordEnabled           bool       `json:"password_enabled"`
    PasswordLastUsed          *time.Time `json:"password_last_used,omitempty"`
    MFAActive                 bool       `json:"mfa_active"`
    AccessKey1Active          bool       `json:"access_key_1_active"`
    AccessKey1LastRotated     *time.Time `json:"access_key_1_last_rotated,omitempty"`
    AccessKey1LastUsedDate    *time.Time `json:"access_key_1_last_used_date,omitempty"`
    AccessKey2Active          bool       `json:"access_key_2_active"`
    AccessKey2LastRotated     *time.Time `json:"access_key_2_last_rotated,omitempty"`
}

IAM operations

List roles

func (c *Client) ListRoles(ctx context.Context) ([]RoleInfo, error) {
    paginator := iam.NewListRolesPaginator(c.iam, &iam.ListRolesInput{})
    
    var roles []RoleInfo
    for paginator.HasMorePages() {
        page, err := paginator.NextPage(ctx)
        if err != nil {
            return roles, fmt.Errorf("failed to list roles: %w", err)
        }
        
        for _, role := range page.Roles {
            roleInfo := RoleInfo{
                RoleName:           aws.ToString(role.RoleName),
                RoleARN:            aws.ToString(role.Arn),
                Path:               aws.ToString(role.Path),
                MaxSessionDuration: aws.ToInt32(role.MaxSessionDuration),
            }
            // ... populate additional fields
            roles = append(roles, roleInfo)
        }
    }
    
    return roles, nil
}

Get role details

func (c *Client) GetRoleDetails(ctx context.Context, roleName string) (*RoleDetail, error) {
    roleResp, err := c.iam.GetRole(ctx, &iam.GetRoleInput{
        RoleName: aws.String(roleName),
    })
    if err != nil {
        return nil, fmt.Errorf("failed to get role %s: %w", roleName, err)
    }
    
    detail := &RoleDetail{/* ... */}
    
    // Get attached policies
    attachedResp, err := c.iam.ListAttachedRolePolicies(ctx, &iam.ListAttachedRolePoliciesInput{
        RoleName: aws.String(roleName),
    })
    if err == nil {
        for _, policy := range attachedResp.AttachedPolicies {
            detail.AttachedPolicies = append(detail.AttachedPolicies, PolicyInfo{
                PolicyName: aws.ToString(policy.PolicyName),
                PolicyARN:  aws.ToString(policy.PolicyArn),
            })
        }
    }
    
    // Get inline policies
    inlineResp, err := c.iam.ListRolePolicies(ctx, &iam.ListRolePoliciesInput{
        RoleName: aws.String(roleName),
    })
    // ... retrieve inline policy documents
    
    return detail, nil
}

Update policies

func (c *Client) CreatePolicyVersion(ctx context.Context, policyARN, document string, setAsDefault bool) error {
    _, err := c.iam.CreatePolicyVersion(ctx, &iam.CreatePolicyVersionInput{
        PolicyArn:      aws.String(policyARN),
        PolicyDocument: aws.String(document),
        SetAsDefault:   setAsDefault,
    })
    return err
}

func (c *Client) UpdateAssumeRolePolicy(ctx context.Context, roleName, document string) error {
    _, err := c.iam.UpdateAssumeRolePolicy(ctx, &iam.UpdateAssumeRolePolicyInput{
        RoleName:       aws.String(roleName),
        PolicyDocument: aws.String(document),
    })
    return err
}

Natural language queries

Use natural language for IAM security analysis:
clanker ask --iam "Which roles have administrative access?"
clanker ask --iam "Find IAM users without MFA enabled"
clanker ask --iam "Show me access keys older than 90 days"
clanker ask --iam "What roles allow cross-account access?"

Best practices

Always run analyze commands before attempting fixes. Review findings manually before applying automated remediation.
Scope IAM analysis to specific roles or policies using --role-arn or --policy-arn flags for faster, focused results.
Automated remediation plans include warnings and notes. Review these carefully, especially for production environments.
Schedule regular credential report analysis to catch credential hygiene issues early:
clanker ask --iam "analyze credential report for security issues"
The IAM agent maintains conversation history per AWS account. Use this for tracking remediation progress over time.

Example workflows

Complete security audit

# 1. Analyze entire account
clanker ask --iam "perform comprehensive security analysis"

# 2. Review findings by severity
clanker ask --iam "show me critical and high severity findings"

# 3. Generate fix plan for highest priority issue
clanker ask --iam "fix the most critical security issue"

# 4. Review and apply fix plan (manual step)
# Apply fix plan via separate command after review

Role-specific security review

# Analyze specific role
clanker ask --iam --role-arn "arn:aws:iam::123456789012:role/LambdaExecutionRole" \
  "analyze this role for security issues"

# Review trust policy
clanker ask --iam --role-arn "arn:aws:iam::123456789012:role/LambdaExecutionRole" \
  "what principals can assume this role?"

# Check for least privilege
clanker ask --iam --role-arn "arn:aws:iam::123456789012:role/LambdaExecutionRole" \
  "does this role follow least privilege principle?"

Credential hygiene

# Generate and analyze credential report
clanker ask --iam "check credential report for security issues"

# Find old access keys
clanker ask --iam "list access keys older than 90 days"

# Find users without MFA
clanker ask --iam "which users don't have MFA enabled?"

Error handling

The IAM agent provides detailed error messages:
if err != nil {
    return &Response{
        Type:  ResponseTypeError,
        Error: fmt.Errorf("failed to analyze role: %w", err),
    }, nil
}
Common errors:
  • Access Denied: IAM client lacks required permissions
  • Role Not Found: Specified role ARN doesn’t exist
  • Invalid Policy Document: Policy JSON is malformed
  • Account Not Accessible: Profile doesn’t have STS permissions
The IAM agent requires significant IAM permissions. For production use, grant least privilege permissions:
  • iam:ListRoles
  • iam:GetRole
  • iam:ListPolicies
  • iam:GetPolicy
  • iam:GetPolicyVersion
  • iam:GetCredentialReport
  • iam:GenerateCredentialReport
For remediation, additional write permissions are required.

Build docs developers (and LLMs) love