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
27 changes: 14 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ When asked to analyze Azure IaC for minimum permissions, follow this process:
### Environment Variables (Required)

```bash
export MPF_SUBSCRIPTIONID="<your-subscription-id>"
export MPF_TENANTID="<your-tenant-id>"
export MPF_SPCLIENTID="<service-principal-client-id>"
export MPF_SPCLIENTSECRET="<service-principal-secret>"
export MPF_SPOBJECTID="<service-principal-object-id>"
# Preferred snake_case form (legacy concatenated names like MPF_SUBSCRIPTIONID still work)
export MPF_SUBSCRIPTION_ID="<your-subscription-id>"
export MPF_TENANT_ID="<your-tenant-id>"
export MPF_SP_CLIENT_ID="<service-principal-client-id>"
export MPF_SP_CLIENT_SECRET="<service-principal-secret>"
export MPF_SP_OBJECT_ID="<service-principal-object-id>"
```

### Service Principal Setup
Expand All @@ -41,11 +42,11 @@ Create a dedicated Service Principal for MPF analysis (it should have NO roles a

```bash
MPF_SP=$(az ad sp create-for-rbac --name "MPF-Analyzer-SP" --skip-assignment)
export MPF_SPCLIENTID=$(echo $MPF_SP | jq -r .appId)
export MPF_SPCLIENTSECRET=$(echo $MPF_SP | jq -r .password)
export MPF_SPOBJECTID=$(az ad sp show --id $MPF_SPCLIENTID --query id -o tsv)
export MPF_TENANTID=$(az account show --query tenantId -o tsv)
export MPF_SUBSCRIPTIONID=$(az account show --query id -o tsv)
export MPF_SP_CLIENT_ID=$(echo $MPF_SP | jq -r .appId)
export MPF_SP_CLIENT_SECRET=$(echo $MPF_SP | jq -r .password)
export MPF_SP_OBJECT_ID=$(az ad sp show --id $MPF_SP_CLIENT_ID --query id -o tsv)
export MPF_TENANT_ID=$(az account show --query tenantId -o tsv)
export MPF_SUBSCRIPTION_ID=$(az account show --query id -o tsv)
```

---
Expand Down Expand Up @@ -90,7 +91,7 @@ azmpf arm \
### Bicep Analysis

```bash
export MPF_BICEPEXECPATH=$(which bicep)
export MPF_BICEP_EXEC_PATH=$(which bicep)

azmpf bicep \
--bicepFilePath ./path/to/main.bicep \
Expand All @@ -102,7 +103,7 @@ azmpf bicep \
### Terraform Analysis

```bash
export MPF_TFPATH=$(which terraform)
export MPF_TF_PATH=$(which terraform)

# Ensure terraform is initialized
cd ./terraform-module-dir
Expand Down Expand Up @@ -177,7 +178,7 @@ For least-privilege access, generate a custom role:
After analysis, delete the Service Principal:

```bash
az ad sp delete --id "$MPF_SPCLIENTID"
az ad sp delete --id "$MPF_SP_CLIENT_ID"
```

---
Expand Down
70 changes: 35 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,21 @@ To run the unit tests, run `task testunit`.
To run the end-to-end tests for ARM, you need to have the following environment variables set, and then execute `task teste2e:arm`:

```shell
# bash
export MPF_SUBSCRIPTIONID="YOUR_SUBSCRIPTION_ID"
export MPF_TENANTID="YOUR_TENANT_ID"
export MPF_SPCLIENTID="YOUR_SP_CLIENT_ID"
export MPF_SPCLIENTSECRET="YOUR_SP_CLIENT_SECRET"
export MPF_SPOBJECTID="YOUR_SP_OBJECT_ID"
# bash (snake_case preferred; legacy names like MPF_SUBSCRIPTIONID still work for the CLI)
export MPF_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
export MPF_TENANT_ID="YOUR_TENANT_ID"
export MPF_SP_CLIENT_ID="YOUR_SP_CLIENT_ID"
export MPF_SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET"
export MPF_SP_OBJECT_ID="YOUR_SP_OBJECT_ID"
```

```powershell
# powershell
$env:MPF_SUBSCRIPTIONID="YOUR_SUBSCRIPTION_ID"
$env:MPF_TENANTID="YOUR_TENANT_ID"
$env:MPF_SPCLIENTID="YOUR_SP_CLIENT_ID"
$env:MPF_SPCLIENTSECRET="YOUR_SP_CLIENT_SECRET"
$env:MPF_SPOBJECTID="YOUR_SP_OBJECT_ID"
$env:MPF_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
$env:MPF_TENANT_ID="YOUR_TENANT_ID"
$env:MPF_SP_CLIENT_ID="YOUR_SP_CLIENT_ID"
$env:MPF_SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET"
$env:MPF_SP_OBJECT_ID="YOUR_SP_OBJECT_ID"
```

```shell
Expand All @@ -130,22 +130,22 @@ To run the end-to-end tests for Bicep, you need to have the following environmen

```shell
# bash
export MPF_SUBSCRIPTIONID="YOUR_SUBSCRIPTION_ID"
export MPF_TENANTID="YOUR_TENANT_ID"
export MPF_SPCLIENTID="YOUR_SP_CLIENT_ID"
export MPF_SPCLIENTSECRET="YOUR_SP_CLIENT_SECRET"
export MPF_SPOBJECTID="YOUR_SP_OBJECT_ID"
export MPF_BICEPEXECPATH="/opt/homebrew/bin/bicep" # Path to the Bicep executable
export MPF_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
export MPF_TENANT_ID="YOUR_TENANT_ID"
export MPF_SP_CLIENT_ID="YOUR_SP_CLIENT_ID"
export MPF_SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET"
export MPF_SP_OBJECT_ID="YOUR_SP_OBJECT_ID"
export MPF_BICEP_EXEC_PATH="/opt/homebrew/bin/bicep" # Path to the Bicep executable
```

```powershell
# powershell
$env:MPF_SUBSCRIPTIONID="YOUR_SUBSCRIPTION_ID"
$env:MPF_TENANTID="YOUR_TENANT_ID"
$env:MPF_SPCLIENTID="YOUR_SP_CLIENT_ID"
$env:MPF_SPCLIENTSECRET="YOUR_SP_CLIENT_SECRET"
$env:MPF_SPOBJECTID="YOUR_SP_OBJECT_ID"
$env:MPF_BICEPEXECPATH=$(where.exe bicep)
$env:MPF_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
$env:MPF_TENANT_ID="YOUR_TENANT_ID"
$env:MPF_SP_CLIENT_ID="YOUR_SP_CLIENT_ID"
$env:MPF_SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET"
$env:MPF_SP_OBJECT_ID="YOUR_SP_OBJECT_ID"
$env:MPF_BICEP_EXEC_PATH=$(where.exe bicep)
```

```shell
Expand All @@ -158,22 +158,22 @@ The Terraform end-to-end tests can take a long time to execute, depending on the

```shell
# bash
export MPF_SUBSCRIPTIONID="YOUR_SUBSCRIPTION_ID"
export MPF_TENANTID="YOUR_TENANT_ID"
export MPF_SPCLIENTID="YOUR_SP_CLIENT_ID"
export MPF_SPCLIENTSECRET="YOUR_SP_CLIENT_SECRET"
export MPF_SPOBJECTID="YOUR_SP_OBJECT_ID"
export MPF_TFPATH=$(which terraform) # Path to the Terraform executable
export MPF_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
export MPF_TENANT_ID="YOUR_TENANT_ID"
export MPF_SP_CLIENT_ID="YOUR_SP_CLIENT_ID"
export MPF_SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET"
export MPF_SP_OBJECT_ID="YOUR_SP_OBJECT_ID"
export MPF_TF_PATH=$(which terraform) # Path to the Terraform executable
```

```powershell
# powershell
$env:MPF_SUBSCRIPTIONID="YOUR_SUBSCRIPTION_ID"
$env:MPF_TENANTID="YOUR_TENANT_ID"
$env:MPF_SPCLIENTID="YOUR_SP_CLIENT_ID"
$env:MPF_SPCLIENTSECRET="YOUR_SP_CLIENT_SECRET"
$env:MPF_SPOBJECTID="YOUR_SP_OBJECT_ID"
$env:MPF_TFPATH=$(where.exe terraform)
$env:MPF_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
$env:MPF_TENANT_ID="YOUR_TENANT_ID"
$env:MPF_SP_CLIENT_ID="YOUR_SP_CLIENT_ID"
$env:MPF_SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET"
$env:MPF_SP_OBJECT_ID="YOUR_SP_OBJECT_ID"
$env:MPF_TF_PATH=$(where.exe terraform)
```

```shell
Expand Down
119 changes: 119 additions & 0 deletions cmd/envbinding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// MIT License
//
// Copyright (c) Microsoft Corporation.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE

package main

import (
"fmt"
"strings"
"unicode"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"

log "github.com/sirupsen/logrus"
)

// camelToSnakeCase converts a camelCase or PascalCase identifier to snake_case.
// Consecutive uppercase acronyms are handled so that "subscriptionID" becomes
// "subscription_id" and "spClientID" becomes "sp_client_id".
func camelToSnakeCase(s string) string {
if s == "" {
return s
}

runes := []rune(s)
var b strings.Builder
b.Grow(len(s) + 4)

for i, r := range runes {
if unicode.IsUpper(r) {
// Insert underscore before this uppercase rune when it starts a new word:
// - previous rune is lowercase (e.g. nID -> n_id), or
// - previous is uppercase and the next is lowercase (end of acronym before a new word).
if i > 0 {
prev := runes[i-1]
nextIsLower := i+1 < len(runes) && unicode.IsLower(runes[i+1])
if unicode.IsLower(prev) || (unicode.IsUpper(prev) && nextIsLower) {
b.WriteByte('_')
}
}
b.WriteRune(unicode.ToLower(r))
continue
}
b.WriteRune(r)
}

return b.String()
}

// envNamesForFlag returns the full environment variable names accepted for a
// given cobra/viper flag name.
//
// Two forms are supported for backward compatibility:
// 1. Legacy concatenated form derived from the flag name as-is, e.g. subscriptionID -> MPF_SUBSCRIPTIONID
// 2. Snake_case form, e.g. subscriptionID -> MPF_SUBSCRIPTION_ID
//
// When both resolve to the same name (single-word flags like "verbose"), only one entry is returned.
// Order is legacy first, then snake_case, so existing deployments keep their current value if both are set.
func envNamesForFlag(flagName string) []string {
legacy := envPrefix + "_" + strings.ToUpper(flagName)
snake := envPrefix + "_" + strings.ToUpper(camelToSnakeCase(flagName))

if legacy == snake {
return []string{legacy}
}
return []string{legacy, snake}
}

// bindFlags applies viper config and environment values to cobra flags that
// were not set on the command line. Each flag accepts both the legacy
// concatenated env var name and the snake_case form (see envNamesForFlag).
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
cmd.Flags().VisitAll(func(f *pflag.Flag) {
configName := f.Name
// If using camelCase in the config file, replace hyphens with a camelCased string.
// Since viper does case-insensitive comparisons, we don't need to bother fixing the case, and only need to remove the hyphens.
if replaceHyphenWithCamelCase {
configName = strings.ReplaceAll(f.Name, "-", "")
}

// Explicit BindEnv is required so both legacy and snake_case env names are
// recognized. When BindEnv is given explicit names they are used as-is
// (prefix is not re-applied).
envNames := envNamesForFlag(configName)
bindArgs := append([]string{configName}, envNames...)
if err := v.BindEnv(bindArgs...); err != nil {
log.Errorf("Error binding env vars for flag %s: %v\n", f.Name, err)
}

// Apply the viper config value to the flag when the flag is not set and viper has a value
if !f.Changed && v.IsSet(configName) {
val := v.Get(configName)
err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
if err != nil {
log.Errorf("Error setting flag %s: %v\n", f.Name, err)
}
}
})
}
Loading
Loading