Repository: Azure/azqr

Issue:
Fail fast for the scenario when values don't match expected format
## Describe the feature
While trying to include or exclude specific subscriptions or resource groups the tool should validate the format of the resource group name provided.

```
azqr:
  include:
    subscriptions:
      - sub-id
    resourceGroups:
      - rg-name
  exclude:
    recommendations:
      - st-006
```
In this case the scan runs successfully but inventory tab is blank. As the filter expects ResourceGroupID which in above case was only ResourceGroupName. The tool should fail early reporting about the expected format for RG.
i.e. if the format doesn't match ` /subscriptions/sub-id/resourceGroups/rg-name` then inform the user about the same.

Discussion:

Solution patch:
diff --git a/internal/models/filters.go b/internal/models/filters.go
index 09a957d..ac2f75d 100644
--- a/internal/models/filters.go
+++ b/internal/models/filters.go
@@ -3,6 +3,7 @@
 package models
 
 import (
+	"fmt"
 	"os"
 	"strings"
 
@@ -111,6 +112,33 @@ func (e *AzqrFilter) IsResourceTypeExcluded(resourceType string) bool {
 	return !ok
 }
 
+// validateResourceGroupID validates that a resource group ID matches the expected ARM format:
+// /subscriptions/{subscription-id}/resourceGroups/{resource-group-name}
+func validateResourceGroupID(resourceGroupID string) error {
+	parts := strings.Split(resourceGroupID, "/")
+	
+	// ARM resource group ID should have exactly 5 parts: ["", "subscriptions", "sub-id", "resourceGroups", "rg-name"]
+	if len(parts) != 5 {
+		return fmt.Errorf("resource group ID '%s' has incorrect format. Expected format: /subscriptions/{subscription-id}/resourceGroups/{resource-group-name}", resourceGroupID)
+	}
+	
+	// Validate the structure
+	if parts[0] != "" || parts[1] != "subscriptions" || parts[3] != "resourceGroups" {
+		return fmt.Errorf("resource group ID '%s' has incorrect format. Expected format: /subscriptions/{subscription-id}/resourceGroups/{resource-group-name}", resourceGroupID)
+	}
+	
+	// Validate subscription ID and resource group name are not empty
+	if parts[2] == "" {
+		return fmt.Errorf("resource group ID '%s' has empty subscription ID. Expected format: /subscriptions/{subscription-id}/resourceGroups/{resource-group-name}", resourceGroupID)
+	}
+	
+	if parts[4] == "" {
+		return fmt.Errorf("resource group ID '%s' has empty resource group name. Expected format: /subscriptions/{subscription-id}/resourceGroups/{resource-group-name}", resourceGroupID)
+	}
+	
+	return nil
+}
+
 func NewFilters() *Filters {
 	filters := &Filters{
 		Azqr: &AzqrFilter{
@@ -152,6 +180,20 @@ func LoadFilters(filterFile string, scannerKeys []string) *Filters {
 		filters.Azqr.iSubscriptions[strings.ToLower(id)] = true
 	}
 
+	// Validate resource group IDs in include list
+	for _, id := range filters.Azqr.Include.ResourceGroups {
+		if err := validateResourceGroupID(id); err != nil {
+			log.Fatal().Err(err).Msgf("invalid resource group ID in include list")
+		}
+	}
+
+	// Validate resource group IDs in exclude list
+	for _, id := range filters.Azqr.Exclude.ResourceGroups {
+		if err := validateResourceGroupID(id); err != nil {
+			log.Fatal().Err(err).Msgf("invalid resource group ID in exclude list")
+		}
+	}
+
 	filters.Azqr.iResourceGroups = make(map[string]bool)
 	for _, id := range filters.Azqr.Include.ResourceGroups {
 		log.Debug().Msgf("Adding resource group to include: %s", id)



Test patch:
diff --git a/internal/models/filters_test.go b/internal/models/filters_test.go
new file mode 100644
index 0000000..7fabed1
--- /dev/null
+++ b/internal/models/filters_test.go
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+package models
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestValidateResourceGroupID(t *testing.T) {
+	tests := []struct {
+		name           string
+		resourceGroupID string
+		expectError    bool
+		errorContains  string
+	}{
+		{
+			name:           "valid resource group ID",
+			resourceGroupID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg",
+			expectError:    false,
+		},
+		{
+			name:           "invalid format - just resource group name",
+			resourceGroupID: "test-rg",
+			expectError:    true,
+			errorContains:  "has incorrect format",
+		},
+		{
+			name:           "invalid format - missing leading slash",
+			resourceGroupID: "subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg",
+			expectError:    true,
+			errorContains:  "has incorrect format",
+		},
+		{
+			name:           "invalid format - missing subscriptions segment",
+			resourceGroupID: "/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg",
+			expectError:    true,
+			errorContains:  "has incorrect format",
+		},
+		{
+			name:           "invalid format - missing resourceGroups segment",
+			resourceGroupID: "/subscriptions/12345678-1234-1234-1234-123456789012/test-rg",
+			expectError:    true,
+			errorContains:  "has incorrect format",
+		},
+		{
+			name:           "invalid format - wrong resourceGroups segment",
+			resourceGroupID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroup/test-rg",
+			expectError:    true,
+			errorContains:  "has incorrect format",
+		},
+		{
+			name:           "invalid format - empty subscription ID",
+			resourceGroupID: "/subscriptions//resourceGroups/test-rg",
+			expectError:    true,
+			errorContains:  "has empty subscription ID",
+		},
+		{
+			name:           "invalid format - empty resource group name",
+			resourceGroupID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/",
+			expectError:    true,
+			errorContains:  "has empty resource group name",
+		},
+		{
+			name:           "invalid format - too many segments",
+			resourceGroupID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers",
+			expectError:    true,
+			errorContains:  "has incorrect format",
+		},
+		{
+			name:           "invalid format - too few segments",
+			resourceGroupID: "/subscriptions/12345678-1234-1234-1234-123456789012",
+			expectError:    true,
+			errorContains:  "has incorrect format",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateResourceGroupID(tt.resourceGroupID)
+			
+			if tt.expectError {
+				if err == nil {
+					t.Errorf("expected error for resourceGroupID '%s' but got none", tt.resourceGroupID)
+				} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
+					t.Errorf("expected error to contain '%s' but got '%s'", tt.errorContains, err.Error())
+				}
+			} else {
+				if err != nil {
+					t.Errorf("unexpected error for resourceGroupID '%s': %v", tt.resourceGroupID, err)
+				}
+			}
+		})
+	}
+}
+
+func TestLoadFiltersValidation(t *testing.T) {
+	// Test with valid resource group IDs - should not panic or error
+	validFilters := &Filters{
+		Azqr: &AzqrFilter{
+			Include: &IncludeFilter{
+				Subscriptions:  []string{},
+				ResourceGroups: []string{"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg"},
+				ResourceTypes:  []string{},
+			},
+			Exclude: &ExcludeFilter{
+				Subscriptions:   []string{},
+				ResourceGroups:  []string{"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/exclude-rg"},
+				Services:        []string{},
+				Recommendations: []string{},
+			},
+		},
+	}
+	
+	// This should not cause any validation errors
+	if err := validateResourceGroupID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg"); err != nil {
+		t.Errorf("valid resource group ID failed validation: %v", err)
+	}
+	
+	if err := validateResourceGroupID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/exclude-rg"); err != nil {
+		t.Errorf("valid resource group ID failed validation: %v", err)
+	}
+	
+	// Test validation of individual components
+	for _, rgID := range validFilters.Azqr.Include.ResourceGroups {
+		if err := validateResourceGroupID(rgID); err != nil {
+			t.Errorf("include resource group validation failed: %v", err)
+		}
+	}
+	
+	for _, rgID := range validFilters.Azqr.Exclude.ResourceGroups {
+		if err := validateResourceGroupID(rgID); err != nil {
+			t.Errorf("exclude resource group validation failed: %v", err)
+		}
+	}
+}
\ No newline at end of file