├── CODEOWNERS ├── docs ├── content │ └── en │ │ ├── search.md │ │ ├── docs │ │ ├── Recommendations │ │ │ └── _index.md │ │ ├── _index.md │ │ ├── Install │ │ │ └── _index.md │ │ ├── Troubleshooting │ │ │ └── _index.md │ │ ├── Reference │ │ │ └── _index.md │ │ └── Contributing │ │ │ └── _index.md │ │ └── _index.md ├── static │ ├── favicons │ │ ├── favicon.ico │ │ └── site.webmanifest │ └── logo │ │ └── azqr_readme.png ├── layouts │ ├── shortcodes │ │ └── include.html │ └── 404.html ├── go.mod ├── assets │ ├── scss │ │ └── _variables_project.scss │ └── icons │ │ └── logo.svg ├── config.yaml ├── go.sum └── package.json ├── internal ├── embeded │ ├── azqr.png │ ├── embeded.go │ └── embeded_test.go ├── viewer │ ├── static │ │ └── fonts │ │ │ ├── bootstrap-icons.woff │ │ │ └── bootstrap-icons.woff2 │ └── server_test.go ├── to │ ├── ptr.go │ ├── string.go │ ├── string_test.go │ └── ptr_test.go ├── graph │ └── azure-orphan-resources │ │ ├── Network │ │ └── kql │ │ │ ├── 8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e.kql │ │ │ ├── 3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d.kql │ │ │ ├── 9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f.kql │ │ │ ├── 1e2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b.kql │ │ │ ├── 0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e.kql │ │ │ ├── 6d7e8f9a-0b1c-2d3e-4f5a-6b7c8d9e0f1a.kql │ │ │ ├── 5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f.kql │ │ │ ├── 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d.kql │ │ │ ├── 7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b.kql │ │ │ ├── 0d1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a.kql │ │ │ ├── 5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b.kql │ │ │ ├── 6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b1c.kql │ │ │ ├── 4b5c6d7e-8f9a-0b1c-2d3e-4f5a6b7c8d9e.kql │ │ │ ├── 9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d.kql │ │ │ ├── 8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c.kql │ │ │ └── 2f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c.kql │ │ ├── Web │ │ ├── kql │ │ │ ├── 3e4f5a6b-7c8d-9e0f-1a2b-3c4d5e6f7a8b.kql │ │ │ ├── 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d.kql │ │ │ └── 2d3e4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a.kql │ │ └── queries.yaml │ │ ├── Compute │ │ ├── kql │ │ │ ├── 2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e.kql │ │ │ └── 3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f.kql │ │ └── queries.yaml │ │ ├── Resources │ │ ├── kql │ │ │ └── 1c2d3e4f-5a6b-7c8d-9e0f-1a2b3c4d5e6f.kql │ │ └── queries.yaml │ │ └── Sql │ │ ├── queries.yaml │ │ └── kql │ │ └── 4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a.kql ├── scanners │ ├── plugins │ │ └── zone │ │ │ └── types.go │ ├── sap │ │ ├── sap.go │ │ └── sap_test.go │ ├── nic │ │ ├── nic.go │ │ └── nic_test.go │ ├── disk │ │ ├── disk.go │ │ └── disk_test.go │ ├── iot │ │ ├── iot.go │ │ └── iot_test.go │ ├── gal │ │ ├── gal.go │ │ └── gal_test.go │ ├── netapp │ │ ├── netapp.go │ │ └── netapp_test.go │ ├── avs │ │ ├── avs.go │ │ └── avs_test.go │ ├── conn │ │ ├── conn.go │ │ └── conn_test.go │ ├── ba │ │ ├── ba.go │ │ └── ba_test.go │ ├── rg │ │ ├── rg.go │ │ └── rg_test.go │ ├── rsv │ │ ├── rsv.go │ │ └── rsv_test.go │ ├── avail │ │ ├── avail.go │ │ └── avail_test.go │ ├── aa │ │ ├── aa.go │ │ └── aa_test.go │ ├── avd │ │ ├── avd.go │ │ └── avd_test.go │ ├── erc │ │ ├── erc.go │ │ └── erc_test.go │ ├── odb │ │ ├── odb.go │ │ └── odb_test.go │ ├── fdfp │ │ ├── fdfp.go │ │ └── fdfp_test.go │ ├── hpc │ │ ├── hpc.go │ │ └── hpc_test.go │ ├── pdnsz │ │ ├── pdnsz.go │ │ └── pdnsz_test.go │ ├── vdpool │ │ ├── vdpool.go │ │ └── vdpool_test.go │ ├── arc │ │ ├── arc.go │ │ └── arc_test.go │ ├── it │ │ └── rules.go │ ├── nsg │ │ └── rules_test.go │ ├── appi │ │ └── rules_test.go │ ├── pip │ │ └── rules_test.go │ ├── vm │ │ └── rules_test.go │ ├── ng │ │ └── rules_test.go │ ├── rt │ │ └── rules_test.go │ ├── pip.go │ ├── log │ │ └── rules_test.go │ └── nw │ │ └── rules_test.go ├── renderers │ ├── types.go │ └── excel │ │ ├── advisor.go │ │ ├── cost.go │ │ ├── arc_sql.go │ │ ├── impacted.go │ │ ├── azure_policy.go │ │ ├── resourceTypes.go │ │ ├── recommendations.go │ │ └── resources.go ├── throttling │ ├── limiter.go │ └── policy.go ├── plugins │ ├── loader.go │ └── internal.go └── models │ └── base_scanner.go ├── scripts ├── update_aprl.sh ├── install.sh ├── install.ps1 ├── test_graph_queries.sh └── verify-checksum.sh ├── .gitmodules ├── .github ├── ISSUE_TEMPLATE │ ├── discussion.md │ ├── config.yml │ ├── proposal.md │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md ├── workflows │ ├── stale.yml │ ├── dependency-review.yml │ └── bump-winget.yml └── dependabot.yml ├── cmd ├── server │ ├── main.go │ └── commands │ │ └── root.go └── azqr │ ├── commands │ ├── hpc.go │ ├── nic.go │ ├── nsg.go │ ├── sap.go │ ├── disk.go │ ├── pip.go │ ├── rt.go │ ├── conn.go │ ├── it.go │ ├── st.go │ ├── afw.go │ ├── iot.go │ ├── kv.go │ ├── netapp.go │ ├── nw.go │ ├── rg.go │ ├── vm.go │ ├── ba.go │ ├── dbw.go │ ├── evh.go │ ├── gal.go │ ├── lb.go │ ├── ng.go │ ├── pep.go │ ├── rsv.go │ ├── sb.go │ ├── sigr.go │ ├── wps.go │ ├── adf.go │ ├── asp.go │ ├── ca.go │ ├── dec.go │ ├── sql.go │ ├── srch.go │ ├── aa.go │ ├── amg.go │ ├── as.go │ ├── avd.go │ ├── avs.go │ ├── logic.go │ ├── odb.go │ ├── pdnsz.go │ ├── vwan.go │ ├── aks.go │ ├── apim.go │ ├── avail.go │ ├── ci.go │ ├── cosmos.go │ ├── cr.go │ ├── erc.go │ ├── log.go │ ├── traf.go │ ├── vgw.go │ ├── vnet.go │ ├── agw.go │ ├── arc.go │ ├── evgd.go │ ├── psql.go │ ├── redis.go │ ├── synw.go │ ├── appcs.go │ ├── appi.go │ ├── mysql.go │ ├── vdpool.go │ ├── vmss.go │ ├── maria.go │ ├── cae.go │ ├── aif.go │ ├── fdfp.go │ ├── hub.go │ ├── afd.go │ ├── types.go │ ├── rules.go │ └── root.go │ └── main.go ├── examples ├── filters │ └── containers-filters.yml ├── plugins │ └── yaml-example │ │ └── kql │ │ └── unused-public-ips.kql └── cicd │ ├── github-actions.yml │ └── azdo-pipeline.yml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── .dockerignore ├── CONTRIBUTING.md ├── .vscode └── launch.json ├── .gitignore ├── LICENSE ├── jumpstart_drops └── azqr-tutorial.json ├── .devcontainer └── devcontainer.json └── SECURITY_VERIFICATION.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners are the maintainers and approvers of this repo 2 | * @cmendible -------------------------------------------------------------------------------- /docs/content/en/search.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Search Results 3 | layout: search 4 | --- 5 | -------------------------------------------------------------------------------- /internal/embeded/azqr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azqr/HEAD/internal/embeded/azqr.png -------------------------------------------------------------------------------- /scripts/update_aprl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git submodule init 4 | git submodule update --remote -------------------------------------------------------------------------------- /docs/static/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azqr/HEAD/docs/static/favicons/favicon.ico -------------------------------------------------------------------------------- /docs/static/logo/azqr_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azqr/HEAD/docs/static/logo/azqr_readme.png -------------------------------------------------------------------------------- /docs/layouts/shortcodes/include.html: -------------------------------------------------------------------------------- 1 | {{ $file := .Get 0 }} {{ if strings.HasSuffix $file ".txt" }} {{ $file | readFile | safeHTML }} {{ end }} -------------------------------------------------------------------------------- /internal/viewer/static/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azqr/HEAD/internal/viewer/static/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /internal/viewer/static/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azqr/HEAD/internal/viewer/static/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "internal/graph/aprl"] 2 | path = internal/graph/aprl 3 | url = https://github.com/Azure/Azure-Proactive-Resiliency-Library-v2.git 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Discussion 3 | about: Start a discussion for azqr 4 | title: '' 5 | labels: kind/discussion 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /internal/to/ptr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package to 5 | 6 | func Ptr[E any](e E) *E { 7 | return &e 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: azqr Repo 4 | url: https://github.com/Azure/azqr 5 | about: Please see our community docs here. 6 | -------------------------------------------------------------------------------- /docs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/docsy-example 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/google/docsy v0.11.0 // indirect 7 | github.com/google/docsy/dependencies v0.7.2 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Proposal 3 | about: Create a proposal for azqr 4 | title: '' 5 | labels: kind/proposal 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the proposal -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about azqr 4 | title: '' 5 | labels: kind/question 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Ask your question here -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Azure/azqr/cmd/server/commands" 5 | ) 6 | 7 | // main is the entry point for the serve executable. 8 | func main() { 9 | commands.Execute() 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Create a Feature Request for azqr 4 | title: '' 5 | labels: kind/enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the feature 11 | 12 | -------------------------------------------------------------------------------- /docs/static/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /examples/filters/containers-filters.yml: -------------------------------------------------------------------------------- 1 | # Including only Azure Kubernetes Service (aks), Container Apps (ca), Container App Environments (cae), Container Instances (ci) and Container Registry (cr) resources. 2 | azqr: 3 | include: 4 | resourceTypes: 5 | - aks 6 | - ca 7 | - cae 8 | - ci 9 | - cr 10 | -------------------------------------------------------------------------------- /docs/content/en/docs/Recommendations/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Recommendations 3 | description: Recommendations 4 | weight: 4 5 | --- 6 | 7 | Azure Quick Review checks the following recommendations for Azure resources. The recommendations are categorized based on their impact and category: 8 | 9 | {{% include "./static/rules.txt" %}} 10 | -------------------------------------------------------------------------------- /docs/layouts/404.html: -------------------------------------------------------------------------------- 1 | {{ define "main" -}} 2 |
3 |

Not found

4 |

Oops! This page doesn't exist. Try going back to the home page.

5 |

You can learn how to make a 404 page like this in Custom 404 Pages.

6 |
7 | {{- end }} 8 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all route tables without subnets 3 | resources 4 | | where type == "microsoft.network/routetables" 5 | | where isnull(properties.subnets) 6 | | project recommendationId="8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e", name, id, tags 7 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all virtual networks without subnets 3 | resources 4 | | where type == "microsoft.network/virtualnetworks" 5 | | where properties.subnets == "[]" 6 | | project recommendationId="3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d", name, id, tags 7 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all load balancers without backend pools 3 | resources 4 | | where type == "microsoft.network/loadbalancers" 5 | | where properties.backendAddressPools == "[]" 6 | | project recommendationId="9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f", name, id, tags 7 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/1e2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all Traffic Manager profiles without endpoints 3 | resources 4 | | where type == "microsoft.network/trafficmanagerprofiles" 5 | | where properties.endpoints == "[]" 6 | | project recommendationId="1e2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b", name, id, tags 7 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Web/kql/3e4f5a6b-7c8d-9e0f-1a2b-3c4d5e6f7a8b.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all expired App Service Certificates 3 | resources 4 | | where type == 'microsoft.web/certificates' 5 | | extend expiresOn = todatetime(properties.expirationDate) 6 | | where expiresOn <= now() 7 | | project recommendationId="3e4f5a6b-7c8d-9e0f-1a2b-3c4d5e6f7a8b", name, id, tags 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/gitleaks/gitleaks 3 | rev: v8.16.3 4 | hooks: 5 | - id: gitleaks 6 | - repo: https://github.com/golangci/golangci-lint 7 | rev: v1.63.4 8 | hooks: 9 | - id: golangci-lint 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.4.0 12 | hooks: 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all DDoS protection plans that are not associated with any virtual networks 3 | resources 4 | | where type == "microsoft.network/ddosprotectionplans" 5 | | where isnull(properties.virtualNetworks) 6 | | project recommendationId="0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", name, id, tags 7 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/6d7e8f9a-0b1c-2d3e-4f5a-6b7c8d9e0f1a.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all IP groups without firewalls or firewall policies 3 | resources 4 | | where type == "microsoft.network/ipgroups" 5 | | where properties.firewalls == "[]" and properties.firewallPolicies == "[]" 6 | | project recommendationId="6d7e8f9a-0b1c-2d3e-4f5a-6b7c8d9e0f1a", name, id, tags 7 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all NAT gateways without subnets 3 | resources 4 | | where type == "microsoft.network/natgateways" 5 | | where isnull(properties.subnets) 6 | | project recommendationId="5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f", name, id, tags, param1=strcat("Sku: ", sku.name), param2=strcat("Tier: ", sku.tier) 7 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all network security groups without associated network interfaces or subnets 3 | resources 4 | | where type == "microsoft.network/networksecuritygroups" and isnull(properties.networkInterfaces) and isnull(properties.subnets) 5 | | project recommendationId="7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", name, id, tags 6 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Compute/kql/2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all availability sets that are not associated with any virtual machines 3 | resources 4 | | where type =~ 'Microsoft.Compute/availabilitySets' 5 | | where properties.virtualMachines == "[]" 6 | | where not(name endswith "-asr") 7 | | project recommendationId="2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e", name, id, tags 8 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Web/kql/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all App Service Plans that have no sites associated with them 3 | resources 4 | | where type =~ "microsoft.web/serverfarms" 5 | | where properties.numberOfSites == 0 6 | | project recommendationId="1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", name, id, tags, param1=strcat("Sku: ", sku.name), param2=strcat("Tier: ", sku.name) 7 | -------------------------------------------------------------------------------- /docs/assets/scss/_variables_project.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Add styles or override variables from the theme here. 3 | */ 4 | 5 | $primary: #004589; 6 | $secondary: #0080ff; 7 | 8 | $light: #89c4ff; 9 | $dark: #001e3b; 10 | 11 | // UI element colors 12 | $td-sidebar-bg-color: rgba($primary, 0.15); 13 | 14 | /* 15 | AZURE COLOR PALETTE 16 | #d8ebff (216,235,255) 17 | #89c4ff (137,196,255) 18 | #0080ff (0,128,255) 19 | #004589 (0,69,137) 20 | #001e3b (0,30,59) 21 | */ -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all private DNS zones without virtual network links 3 | resources 4 | | where type == "microsoft.network/privatednszones" 5 | | where properties.numberOfVirtualNetworkLinks == 0 6 | | project recommendationId="7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b", name, id, tags, param1=strcat("NumberOfRecordSets: ", properties.numberOfRecordSets) 7 | -------------------------------------------------------------------------------- /internal/scanners/plugins/zone/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package zone 5 | 6 | // zoneMappingResult represents a single logical-to-physical zone mapping for a location 7 | type zoneMappingResult struct { 8 | subscriptionID string 9 | subscriptionName string 10 | location string 11 | displayName string 12 | logicalZone string 13 | physicalZone string 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in azqr 4 | title: '' 5 | labels: kind/bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Expected Behavior 11 | 12 | 13 | 14 | ## Actual Behavior 15 | 16 | 17 | 18 | ## Steps to Reproduce the Problem 19 | 20 | 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns -------------------------------------------------------------------------------- /internal/embeded/embeded.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package embeded 5 | 6 | import ( 7 | "embed" 8 | ) 9 | 10 | //go:embed *.png 11 | var embededFiles embed.FS 12 | 13 | // GetTemplates - Returns the template for the given name 14 | func GetTemplates(templateName string) []byte { 15 | data, err := embededFiles.ReadFile(templateName) 16 | if err != nil { 17 | return nil 18 | } 19 | return data 20 | } 21 | -------------------------------------------------------------------------------- /cmd/azqr/commands/hpc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(hpcCmd) 12 | } 13 | 14 | var hpcCmd = &cobra.Command{ 15 | Use: "hpc", 16 | Short: "Scan HPC", 17 | Long: "Scan HPC", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"hpc"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/nic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(nicCmd) 12 | } 13 | 14 | var nicCmd = &cobra.Command{ 15 | Use: "nic", 16 | Short: "Scan NICs", 17 | Long: "Scan NICs", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"nic"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/nsg.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(nsgCmd) 12 | } 13 | 14 | var nsgCmd = &cobra.Command{ 15 | Use: "nsg", 16 | Short: "Scan NSG", 17 | Long: "Scan NSG", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"nsg"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/sap.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(sapCmd) 12 | } 13 | 14 | var sapCmd = &cobra.Command{ 15 | Use: "sap", 16 | Short: "Scan SAP", 17 | Long: "Scan SAP", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"sap"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/disk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(diskCmd) 12 | } 13 | 14 | var diskCmd = &cobra.Command{ 15 | Use: "disk", 16 | Short: "Scan Disk", 17 | Long: "Scan Disk", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"disk"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/Azure/azqr/cmd/azqr/commands" 8 | 9 | // Import internal plugins to register them 10 | _ "github.com/Azure/azqr/internal/scanners/plugins/carbon" 11 | _ "github.com/Azure/azqr/internal/scanners/plugins/openai" 12 | _ "github.com/Azure/azqr/internal/scanners/plugins/zone" 13 | ) 14 | 15 | func main() { 16 | commands.Execute() 17 | } 18 | -------------------------------------------------------------------------------- /cmd/azqr/commands/pip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(pipCmd) 12 | } 13 | 14 | var pipCmd = &cobra.Command{ 15 | Use: "pip", 16 | Short: "Scan Public IP", 17 | Long: "Scan Public IP", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"pip"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/rt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(rtCmd) 12 | } 13 | 14 | var rtCmd = &cobra.Command{ 15 | Use: "rt", 16 | Short: "Scan Route Table", 17 | Long: "Scan Route Table", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"rt"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/conn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(conCmd) 12 | } 13 | 14 | var conCmd = &cobra.Command{ 15 | Use: "con", 16 | Short: "Scan Connection", 17 | Long: "Scan Connection", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"con"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/it.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(itCmd) 12 | } 13 | 14 | var itCmd = &cobra.Command{ 15 | Use: "it", 16 | Short: "Scan Image Template", 17 | Long: "Scan Image Template", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"it"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/st.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(stCmd) 12 | } 13 | 14 | var stCmd = &cobra.Command{ 15 | Use: "st", 16 | Short: "Scan Azure Storage", 17 | Long: "Scan Azure Storage", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"st"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/0d1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all Front Door WAF policies that are not associated without associations 3 | resources 4 | | where type == "microsoft.network/frontdoorwebapplicationfirewallpolicies" 5 | | where properties.frontendEndpointLinks== "[]" and properties.securityPolicyLinks == "[]" 6 | | project recommendationId="0d1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a", name, id, tags, param1=strcat("Sku: ", sku.name) 7 | -------------------------------------------------------------------------------- /cmd/azqr/commands/afw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(afwCmd) 12 | } 13 | 14 | var afwCmd = &cobra.Command{ 15 | Use: "afw", 16 | Short: "Scan Azure Firewall", 17 | Long: "Scan Azure Firewall", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"afw"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/iot.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(iotCmd) 12 | } 13 | 14 | var iotCmd = &cobra.Command{ 15 | Use: "iot", 16 | Short: "Scan Azure IoT Hub", 17 | Long: "Scan Azure IoT Hub", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"iot"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/kv.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(kvCmd) 12 | } 13 | 14 | var kvCmd = &cobra.Command{ 15 | Use: "kv", 16 | Short: "Scan Azure Key Vault", 17 | Long: "Scan Azure Key Vault", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"kv"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/netapp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(netappCmd) 12 | } 13 | 14 | var netappCmd = &cobra.Command{ 15 | Use: "netapp", 16 | Short: "Scan NetApp", 17 | Long: "Scan NetApp", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"netapp"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/nw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(nwCmd) 12 | } 13 | 14 | var nwCmd = &cobra.Command{ 15 | Use: "nw", 16 | Short: "Scan Network Watcher", 17 | Long: "Scan Network Watcher", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"nw"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/rg.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(rgCmd) 12 | } 13 | 14 | var rgCmd = &cobra.Command{ 15 | Use: "rg", 16 | Short: "Scan Resource Groups", 17 | Long: "Scan Resource Groups", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"rg"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/vm.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(vmCmd) 12 | } 13 | 14 | var vmCmd = &cobra.Command{ 15 | Use: "vm", 16 | Short: "Scan Virtual Machine", 17 | Long: "Scan Virtual Machine", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"vm"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/ba.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(baCmd) 12 | } 13 | 14 | var baCmd = &cobra.Command{ 15 | Use: "ba", 16 | Short: "Scan Azure Batch Account", 17 | Long: "Scan Azure Batch Account", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"ba"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/dbw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(dbwCmd) 12 | } 13 | 14 | var dbwCmd = &cobra.Command{ 15 | Use: "dbw", 16 | Short: "Scan Azure Databricks", 17 | Long: "Scan Azure Databricks", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"dbw"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/evh.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(evhCmd) 12 | } 13 | 14 | var evhCmd = &cobra.Command{ 15 | Use: "evh", 16 | Short: "Scan Azure Event Hubs", 17 | Long: "Scan Azure Event Hubs", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"evh"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/gal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(galCmd) 12 | } 13 | 14 | var galCmd = &cobra.Command{ 15 | Use: "gal", 16 | Short: "Scan Azure Galleries", 17 | Long: "Scan Azure Galleries", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"gal"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/lb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(lbCmd) 12 | } 13 | 14 | var lbCmd = &cobra.Command{ 15 | Use: "lb", 16 | Short: "Scan Azure Load Balancer", 17 | Long: "Scan Azure Load Balancer", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"lb"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/ng.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(ngCmd) 12 | } 13 | 14 | var ngCmd = &cobra.Command{ 15 | Use: "ng", 16 | Short: "Scan Azure NAT Gateway", 17 | Long: "Scan Azure NAT Gateway", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"ng"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/pep.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(pepCmd) 12 | } 13 | 14 | var pepCmd = &cobra.Command{ 15 | Use: "pep", 16 | Short: "Scan Private Endpoint", 17 | Long: "Scan Private Endpoint", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"pep"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/rsv.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(rsvCmd) 12 | } 13 | 14 | var rsvCmd = &cobra.Command{ 15 | Use: "rsv", 16 | Short: "Scan Recovery Service", 17 | Long: "Scan Recovery Service", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"rsv"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/sb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(sbCmd) 12 | } 13 | 14 | var sbCmd = &cobra.Command{ 15 | Use: "sb", 16 | Short: "Scan Azure Service Bus", 17 | Long: "Scan Azure Service Bus", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"sb"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/sigr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(sigrCmd) 12 | } 13 | 14 | var sigrCmd = &cobra.Command{ 15 | Use: "sigr", 16 | Short: "Scan Azure SignalR", 17 | Long: "Scan Azure SignalR", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"sigr"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/wps.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(wpsCmd) 12 | } 13 | 14 | var wpsCmd = &cobra.Command{ 15 | Use: "wps", 16 | Short: "Scan Azure Web PubSub", 17 | Long: "Scan Azure Web PubSub", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"wps"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/adf.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(adfCmd) 12 | } 13 | 14 | var adfCmd = &cobra.Command{ 15 | Use: "adf", 16 | Short: "Scan Azure Data Factory", 17 | Long: "Scan Azure Data Factory", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"adf"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/asp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(planCmd) 12 | } 13 | 14 | var planCmd = &cobra.Command{ 15 | Use: "asp", 16 | Short: "Scan Azure App Service", 17 | Long: "Scan Azure App Service", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"asp"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/ca.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(caCmd) 12 | } 13 | 14 | var caCmd = &cobra.Command{ 15 | Use: "ca", 16 | Short: "Scan Azure Container Apps", 17 | Long: "Scan Azure Container Apps", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"ca"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/dec.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(decCmd) 12 | } 13 | 14 | var decCmd = &cobra.Command{ 15 | Use: "dec", 16 | Short: "Scan Azure Data Explorer", 17 | Long: "Scan Azure Data Explorer", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"dec"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/sql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(sqlCmd) 12 | } 13 | 14 | var sqlCmd = &cobra.Command{ 15 | Use: "sql", 16 | Short: "Scan Azure SQL Database", 17 | Long: "Scan Azure SQL Database", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"sql"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/srch.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(srchCmd) 12 | } 13 | 14 | var srchCmd = &cobra.Command{ 15 | Use: "srch", 16 | Short: "Scan Azure AI Search", 17 | Long: "Scan Azure AI Search", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"srch"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/aa.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(aaCmd) 12 | } 13 | 14 | var aaCmd = &cobra.Command{ 15 | Use: "aa", 16 | Short: "Scan Azure Automation Account", 17 | Long: "Scan Azure Automation Account", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"aa"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/amg.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(amgCmd) 12 | } 13 | 14 | var amgCmd = &cobra.Command{ 15 | Use: "amg", 16 | Short: "Scan Azure Managed Grafana", 17 | Long: "Scan Azure Managed Grafana", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"amg"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/as.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(asCmd) 12 | } 13 | 14 | var asCmd = &cobra.Command{ 15 | Use: "as", 16 | Short: "Scan Azure Analysis Service", 17 | Long: "Scan Azure Analysis Service", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"as"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/avd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(avdCmd) 12 | } 13 | 14 | var avdCmd = &cobra.Command{ 15 | Use: "avd", 16 | Short: "Scan Azure Virtual Desktop", 17 | Long: "Scan Azure Virtual Desktop", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"avd"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/avs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(avsCmd) 12 | } 13 | 14 | var avsCmd = &cobra.Command{ 15 | Use: "avs", 16 | Short: "Scan Azure VMware Solution", 17 | Long: "Scan Azure VMware Solution", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"avs"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/logic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(logicCmd) 12 | } 13 | 14 | var logicCmd = &cobra.Command{ 15 | Use: "logic", 16 | Short: "Scan Azure Logic Apps", 17 | Long: "Scan Azure Logic Apps", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"logic"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/odb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(odbCmd) 12 | } 13 | 14 | var odbCmd = &cobra.Command{ 15 | Use: "odb", 16 | Short: "Scan Oracle Database@Azure", 17 | Long: "Scan Oracle Database@Azure", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"odb"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/pdnsz.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(pdnszCmd) 12 | } 13 | 14 | var pdnszCmd = &cobra.Command{ 15 | Use: "pdnsz", 16 | Short: "Scan Private DNS Zone", 17 | Long: "Scan Private DNS Zone", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"pdnsz"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/vwan.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(vwanCmd) 12 | } 13 | 14 | var vwanCmd = &cobra.Command{ 15 | Use: "vwan", 16 | Short: "Scan Azure Virtual WAN", 17 | Long: "Scan Azure Virtual WAN", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"vwan"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/aks.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(aksCmd) 12 | } 13 | 14 | var aksCmd = &cobra.Command{ 15 | Use: "aks", 16 | Short: "Scan Azure Kubernetes Service", 17 | Long: "Scan Azure Kubernetes Service", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"aks"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/apim.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(apimCmd) 12 | } 13 | 14 | var apimCmd = &cobra.Command{ 15 | Use: "apim", 16 | Short: "Scan Azure API Management", 17 | Long: "Scan Azure API Management", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"apim"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/avail.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(availCmd) 12 | } 13 | 14 | var availCmd = &cobra.Command{ 15 | Use: "avail", 16 | Short: "Scan Availability Sets", 17 | Long: "Scan Availability Sets", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"avail"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/ci.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(ciCmd) 12 | } 13 | 14 | var ciCmd = &cobra.Command{ 15 | Use: "ci", 16 | Short: "Scan Azure Container Instances", 17 | Long: "Scan Azure Container Instances", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"ci"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/cosmos.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(cosmosCmd) 12 | } 13 | 14 | var cosmosCmd = &cobra.Command{ 15 | Use: "cosmos", 16 | Short: "Scan Azure Cosmos DB", 17 | Long: "Scan Azure Cosmos DB", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"cosmos"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/cr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(crCmd) 12 | } 13 | 14 | var crCmd = &cobra.Command{ 15 | Use: "cr", 16 | Short: "Scan Azure Container Registries", 17 | Long: "Scan Azure Container Registries", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"cr"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/erc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(ercCmd) 12 | } 13 | 14 | var ercCmd = &cobra.Command{ 15 | Use: "erc", 16 | Short: "Scan Express Route Circuits", 17 | Long: "Scan Express Route Circuits", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"erc"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(logCmd) 12 | } 13 | 14 | var logCmd = &cobra.Command{ 15 | Use: "log", 16 | Short: "Scan Log Analytics workspace", 17 | Long: "Scan Log Analytics workspace", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"log"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/traf.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(trafCmd) 12 | } 13 | 14 | var trafCmd = &cobra.Command{ 15 | Use: "traf", 16 | Short: "Scan Azure Traffic Manager", 17 | Long: "Scan Azure Traffic Manager", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"traf"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/vgw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(vgwCmd) 12 | } 13 | 14 | var vgwCmd = &cobra.Command{ 15 | Use: "vgw", 16 | Short: "Scan Virtual Network Gateway", 17 | Long: "Scan Virtual Network Gateway", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"vgw"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/vnet.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(vnetCmd) 12 | } 13 | 14 | var vnetCmd = &cobra.Command{ 15 | Use: "vnet", 16 | Short: "Scan Azure Virtual Network", 17 | Long: "Scan Azure Virtual Network", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"vnet"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /docs/config.yaml: -------------------------------------------------------------------------------- 1 | # THIS IS A TEST CONFIG ONLY! 2 | # FOR THE CONFIGURATION OF YOUR SITE USE hugo.yaml. 3 | # 4 | # As of Docsy 0.7.0, Hugo 0.110.0 or later must be used. 5 | # 6 | # The sole purpose of this config file is to detect Hugo-module builds that use 7 | # an older version of Hugo. 8 | # 9 | # DO NOT add any config parameters to this file. You can safely delete this file 10 | # if your project is using the required Hugo version. 11 | 12 | module: 13 | hugoVersion: 14 | extended: true 15 | min: 0.110.0 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for kubelogin 2 | # This Dockerfile copies a pre-built binary into a minimal scratch image. 3 | # The binary should be built before running docker build 4 | ARG BUILDPLATFORM=linux/amd64 5 | FROM --platform=$BUILDPLATFORM scratch 6 | 7 | # Build arguments for multi-architecture support 8 | ARG TARGETARCH=amd64 9 | 10 | # Copy the pre-built binary from local build to /usr/local/bin 11 | COPY bin/linux_${TARGETARCH}/azqr /usr/local/bin/azqr 12 | 13 | # Set the entrypoint 14 | ENTRYPOINT ["/usr/local/bin/azqr"] -------------------------------------------------------------------------------- /cmd/azqr/commands/agw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(agwCmd) 12 | } 13 | 14 | var agwCmd = &cobra.Command{ 15 | Use: "agw", 16 | Short: "Scan Azure Application Gateway", 17 | Long: "Scan Azure Application Gateway", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"agw"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/arc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(arcCmd) 12 | } 13 | 14 | var arcCmd = &cobra.Command{ 15 | Use: "arc", 16 | Short: "Scan Azure Arc-enabled machines", 17 | Long: "Scan Azure Arc-enabled machines", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"arc"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/evgd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(evgdCmd) 12 | } 13 | 14 | var evgdCmd = &cobra.Command{ 15 | Use: "evgd", 16 | Short: "Scan Azure Event Grid Domains", 17 | Long: "Scan Azure Event Grid Domains", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"evgd"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/psql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(psqlCmd) 12 | } 13 | 14 | var psqlCmd = &cobra.Command{ 15 | Use: "psql", 16 | Short: "Scan Azure Database for psql", 17 | Long: "Scan Azure Database for psql", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"psql"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/redis.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(redisCmd) 12 | } 13 | 14 | var redisCmd = &cobra.Command{ 15 | Use: "redis", 16 | Short: "Scan Azure Cache for Redis", 17 | Long: "Scan Azure Cache for Redis", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"redis"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/synw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(synwCmd) 12 | } 13 | 14 | var synwCmd = &cobra.Command{ 15 | Use: "synw", 16 | Short: "Scan Azure Synapse Workspace", 17 | Long: "Scan Azure Synapse Workspace", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"synw"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/appcs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(appcsCmd) 12 | } 13 | 14 | var appcsCmd = &cobra.Command{ 15 | Use: "appcs", 16 | Short: "Scan Azure App Configuration", 17 | Long: "Scan Azure App Configuration", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"appcs"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/appi.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(appiCmd) 12 | } 13 | 14 | var appiCmd = &cobra.Command{ 15 | Use: "appi", 16 | Short: "Scan Azure Application Insights", 17 | Long: "Scan Azure Application Insights", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"appi"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/mysql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(mysqlCmd) 12 | } 13 | 14 | var mysqlCmd = &cobra.Command{ 15 | Use: "mysql", 16 | Short: "Scan Azure Database for MySQL", 17 | Long: "Scan Azure Database for MySQL", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"mysql"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/vdpool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(vdPoolCmd) 12 | } 13 | 14 | var vdPoolCmd = &cobra.Command{ 15 | Use: "vdpool", 16 | Short: "Scan Azure Virtual Desktop", 17 | Long: "Scan Azure Virtual Desktop", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"vdpool"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/vmss.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(vmssCmd) 12 | } 13 | 14 | var vmssCmd = &cobra.Command{ 15 | Use: "vmss", 16 | Short: "Scan Virtual Machine Scale Set", 17 | Long: "Scan Virtual Machine Scale Set", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"vmss"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/maria.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(mariaCmd) 12 | } 13 | 14 | var mariaCmd = &cobra.Command{ 15 | Use: "maria", 16 | Short: "Scan Azure Database for MariaDB", 17 | Long: "Scan Azure Database for MariaDB", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"maria"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/cae.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(caeCmd) 12 | } 13 | 14 | var caeCmd = &cobra.Command{ 15 | Use: "cae", 16 | Short: "Scan Azure Container Apps Environment", 17 | Long: "Scan Azure Container Apps Environment", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"cae"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/aif.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(cogCmd) 12 | } 13 | 14 | var cogCmd = &cobra.Command{ 15 | Use: "aif", 16 | Short: "Scan Azure AI Foundry and Cognitive Services", 17 | Long: "Scan Azure AI and Cognitive Services", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"aif"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /cmd/azqr/commands/fdfp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(fdfpCmd) 12 | } 13 | 14 | var fdfpCmd = &cobra.Command{ 15 | Use: "fdfp", 16 | Short: "Scan Front Door Web Application Policy", 17 | Long: "Scan Front Door Web Application Policy", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"fdfp"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all public IP addresses that are not associated with any resources 3 | resources 4 | | where type == "microsoft.network/publicipaddresses" 5 | | where properties.ipConfiguration == "" and properties.natGateway == "" and properties.publicIPPrefix == "" 6 | | project recommendationId="5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", name, id, tags, param1=strcat("Sku: ", sku.name), param2=strcat("AllocationMethod: ", properties.publicIPAllocationMethod) 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | _Please explain the changes you've made_ 4 | 5 | ## Issue reference 6 | 7 | We strive to have all PR being opened based on an issue, where the problem or feature have been discussed prior to implementation. 8 | 9 | Please reference the issue this PR will close: #_[issue number]_ 10 | 11 | ## Checklist 12 | 13 | Please make sure you've completed the relevant tasks for this PR, out of the following list: 14 | 15 | * [ ] Code compiles correctly 16 | * [ ] Created/updated tests 17 | * [ ] Unit tests passing 18 | -------------------------------------------------------------------------------- /cmd/azqr/commands/hub.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | scanCmd.AddCommand(hubCmd) 12 | } 13 | 14 | var hubCmd = &cobra.Command{ 15 | Use: "hub", 16 | Short: "Scan AI Foundry Hub and Azure Machine Learning Workspaces", 17 | Long: "Scan AI Foundry Hub and Azure Machine Learning Workspaces", 18 | Args: cobra.NoArgs, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | scan(cmd, []string{"hub"}) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /examples/plugins/yaml-example/kql/unused-public-ips.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Find all public IP addresses that are not associated with any resource 3 | resources 4 | | where type =~ 'Microsoft.Network/publicIPAddresses' 5 | | where properties.ipConfiguration == "" or isnull(properties.ipConfiguration) 6 | | where properties.natGateway == "" or isnull(properties.natGateway) 7 | | project recommendationId="yaml-002-unused-public-ips", name, id, tags, 8 | param1=strcat("SKU: ", sku.name), 9 | param2=strcat("Allocation: ", properties.publicIPAllocationMethod) 10 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b1c.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all network interfaces that are not associated with any virtual machines, private endpoints, or private link services 3 | resources 4 | | where type has "microsoft.network/networkinterfaces" 5 | | where isnull(properties.privateEndpoint) 6 | | where isnull(properties.privateLinkService) 7 | | where properties.hostedWorkloads == "[]" 8 | | where properties !has 'virtualmachine' 9 | | project recommendationId="6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b1c", name, id, tags 10 | -------------------------------------------------------------------------------- /internal/to/string.go: -------------------------------------------------------------------------------- 1 | package to 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | func String(i interface{}) string { 11 | if i == nil { 12 | return "" 13 | } 14 | 15 | switch v := i.(type) { 16 | case string: 17 | return v 18 | case int: 19 | return fmt.Sprintf("%d", v) 20 | case bool: 21 | return fmt.Sprintf("%t", v) 22 | default: 23 | jsonStr, err := json.Marshal(i) 24 | if err != nil { 25 | log.Fatal().Err(err).Msg("Unsupported type found in ARG query result") 26 | } 27 | return string(jsonStr) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Resources/kql/1c2d3e4f-5a6b-7c8d-9e0f-1a2b3c4d5e6f.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all empty Resource Groups 3 | ResourceContainers 4 | | where type == "microsoft.resources/subscriptions/resourcegroups" 5 | | extend rgAndSub = strcat(resourceGroup, "--", subscriptionId) 6 | | join kind=leftouter ( 7 | Resources 8 | | extend rgAndSub = strcat(resourceGroup, "--", subscriptionId) 9 | | summarize count() by rgAndSub 10 | ) on rgAndSub 11 | | where isnull(count_) 12 | | project recommendationId="1c2d3e4f-5a6b-7c8d-9e0f-1a2b3c4d5e6f", name, id, tags 13 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | issues: write 14 | steps: 15 | - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 16 | with: 17 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 18 | days-before-stale: 30 19 | days-before-close: 5 -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v jq &> /dev/null || ! command -v unzip &> /dev/null || ! command -v wget &> /dev/null 4 | then 5 | echo "jq, unzip or wget could not be found, please install them." 6 | exit 7 | fi 8 | 9 | arch=$(uname -m) 10 | if [ "$arch" == "aarch64" ]; then 11 | arch="arm64" 12 | else 13 | arch="amd64" 14 | fi 15 | 16 | latest_azqr=$(curl -sL https://api.github.com/repos/Azure/azqr/releases/latest | jq -r ".tag_name" | cut -c1-) 17 | wget https://github.com/Azure/azqr/releases/download/$latest_azqr/azqr-linux-$arch.zip -O azqr.zip 18 | unzip -uj -qq azqr.zip 19 | rm azqr.zip 20 | chmod +x azqr 21 | ./azqr --version 22 | -------------------------------------------------------------------------------- /cmd/azqr/commands/afd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // init initializes the afd command and adds it to the scan command. 11 | func init() { 12 | scanCmd.AddCommand(afdCmd) 13 | } 14 | 15 | // afdCmd represents the afd command. 16 | var afdCmd = &cobra.Command{ 17 | Use: "afd", 18 | Short: "Scan Azure Front Door", 19 | Long: "Scan Azure Front Door", 20 | Args: cobra.NoArgs, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | // Call the scan function with the "afd" argument. 23 | scan(cmd, []string{"afd"}) 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /cmd/azqr/commands/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func init() { 14 | rootCmd.AddCommand(typesCmd) 15 | } 16 | 17 | var typesCmd = &cobra.Command{ 18 | Use: "types", 19 | Short: "Print all supported azure resource types", 20 | Long: "Print all supported azure resource types", 21 | Args: cobra.NoArgs, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | st := renderers.SupportedTypes{} 24 | output := st.GetAll() 25 | fmt.Println(output) 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Sql/queries.yaml: -------------------------------------------------------------------------------- 1 | - description: SQL elastic pool without databases 2 | aprlGuid: 4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a 3 | recommendationTypeId: null 4 | recommendationControl: Governance 5 | recommendationImpact: Medium 6 | recommendationResourceType: Microsoft.Sql/servers/elasticpools 7 | recommendationMetadataState: Active 8 | longDescription: | 9 | SQL elastic pool without databases. 10 | potentialBenefits: Identifies unused resources 11 | pgVerified: false 12 | automationAvailable: false 13 | tags: [] 14 | learnMoreLink: 15 | - name: SQL elastic pool 16 | url: "https://learn.microsoft.com/en-us/azure/azure-sql/database/elastic-pool-overview" 17 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Resources/queries.yaml: -------------------------------------------------------------------------------- 1 | - description: Resource Groups without resources 2 | aprlGuid: 1c2d3e4f-5a6b-7c8d-9e0f-1a2b3c4d5e6f 3 | recommendationTypeId: null 4 | recommendationControl: Governance 5 | recommendationImpact: Medium 6 | recommendationResourceType: Microsoft.Resources/resourceGroups 7 | recommendationMetadataState: Active 8 | longDescription: | 9 | Resource Groups without resources. 10 | potentialBenefits: Identifies unused resources 11 | pgVerified: false 12 | automationAvailable: false 13 | tags: [] 14 | learnMoreLink: 15 | - name: Resource Groups 16 | url: "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/overview" 17 | -------------------------------------------------------------------------------- /docs/content/en/docs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Azure Quick Review 3 | linkTitle: Documentation 4 | menu: {main: {weight: 20}} 5 | weight: 20 6 | --- 7 | 8 | {{% pageinfo %}} 9 | **Azure Quick Review!** — Analyze Azure resources and identify whether they comply with Azure's best practices and recommendations. 10 | {{% /pageinfo %}} 11 | 12 | **Azure Quick Review (azqr)** is a command-line interface (CLI) tool specifically designed to analyze Azure resources and identify whether they comply with Azure's best practices and recommendations. Its primary purpose is to provide users with a detailed overview of their Azure resources, enabling them to easily identify any non-compliant configurations or potential areas for improvement. 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Docker ignore file for kubelogin 2 | # Ignore development and build files that are not needed in Docker context 3 | 4 | # Version control 5 | .git 6 | .gitignore 7 | .gitmodules 8 | .precommit-config.yaml 9 | 10 | # Documentation 11 | docs/ 12 | CODE_OF_CONDUCT.md 13 | CODEOWNERS 14 | CONTRIBUTING.md 15 | LICENSE 16 | README.md 17 | SECURITY.md 18 | jumpstart_drops/ 19 | 20 | # Examples 21 | examples/ 22 | 23 | # Data 24 | data/ 25 | 26 | # Scripts 27 | scripts/ 28 | 29 | # Test files 30 | *_test.go 31 | **/*_test.go 32 | **/testdata/ 33 | **/*VCR.yaml 34 | 35 | # Development files 36 | .github/ 37 | 38 | # IDE files 39 | .vscode/ 40 | .idea/ 41 | *.swp 42 | *.swo 43 | *~ 44 | 45 | # OS files 46 | .DS_Store 47 | Thumbs.db -------------------------------------------------------------------------------- /internal/renderers/types.go: -------------------------------------------------------------------------------- 1 | package renderers 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | type SupportedTypes struct{} 11 | 12 | func (t SupportedTypes) GetAll() string { 13 | output := fmt.Sprintln("Abbreviation | Resource Type ") 14 | output += fmt.Sprintln("---|---") 15 | keys := make([]string, 0, len(models.ScannerList)) 16 | for key := range models.ScannerList { 17 | keys = append(keys, key) 18 | } 19 | sort.Strings(keys) 20 | for _, key := range keys { 21 | for _, t := range models.ScannerList[key] { 22 | for _, rt := range t.ResourceTypes() { 23 | output += fmt.Sprintf("%s | %s", key, rt) 24 | output += fmt.Sprintln() 25 | } 26 | } 27 | } 28 | return output 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/sap/sap.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package sap 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["sap"] = []models.IAzureScanner{NewSAPScanner()} 12 | } 13 | 14 | // NewSAPScanner creates a new SAPScanner 15 | func NewSAPScanner() *SAPScanner { 16 | return &SAPScanner{ 17 | BaseScanner: models.NewBaseScanner("Specialized.Workload/SAP"), 18 | } 19 | } 20 | 21 | // SAPScanner - Scanner for SAP 22 | type SAPScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the SAP Scanner 27 | func (a *SAPScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/nic/nic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package nic 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["nic"] = []models.IAzureScanner{NewNICScanner()} 12 | } 13 | 14 | // NewNICScanner creates a new NICScanner 15 | func NewNICScanner() *NICScanner { 16 | return &NICScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Network/networkInterfaces"), 18 | } 19 | } 20 | 21 | // NICScanner - Scanner for NIC 22 | type NICScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the NIC Scanner 27 | func (a *NICScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/disk/disk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package disk 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["disk"] = []models.IAzureScanner{NewDiskScanner()} 12 | } 13 | 14 | // NewDiskScanner creates a new DiskScanner 15 | func NewDiskScanner() *DiskScanner { 16 | return &DiskScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Compute/disks"), 18 | } 19 | } 20 | 21 | // DiskScanner - Scanner for Disk 22 | type DiskScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Disk Scanner 27 | func (a *DiskScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/azqr/commands/rules.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func init() { 14 | rootCmd.PersistentFlags().BoolP("json", "j", false, "Output rules list in JSON format") 15 | rootCmd.AddCommand(rulesCmd) 16 | } 17 | 18 | var rulesCmd = &cobra.Command{ 19 | Use: "rules", 20 | Short: "Print all recommendations", 21 | Long: "Print all recommendations as markdown table", 22 | Args: cobra.NoArgs, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | oj, _ := cmd.Flags().GetBool("json") 25 | output := renderers.GetAllRecommendations(!oj) 26 | fmt.Println(output) 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /internal/scanners/iot/iot.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package iot 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["iot"] = []models.IAzureScanner{NewIoTHubScanner()} 12 | } 13 | 14 | // NewIoTHubScanner creates a new IoTHubScanner 15 | func NewIoTHubScanner() *IoTHubScanner { 16 | return &IoTHubScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Devices/IotHubs"), 18 | } 19 | } 20 | 21 | // IoTHubScanner - Scanner for IoT Hub 22 | type IoTHubScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the IoT Hub Scanner 27 | func (a *IoTHubScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/gal/gal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package gal 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["gal"] = []models.IAzureScanner{NewGalleryScanner()} 12 | } 13 | 14 | // NewGalleryScanner creates a new GalleryScanner 15 | func NewGalleryScanner() *GalleryScanner { 16 | return &GalleryScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Compute/galleries"), 18 | } 19 | } 20 | 21 | // GalleryScanner - Scanner for Gallery 22 | type GalleryScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Gallery Scanner 27 | func (a *GalleryScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: daily 12 | 13 | - package-ecosystem: gomod 14 | directory: /docs 15 | schedule: 16 | interval: daily 17 | 18 | - package-ecosystem: npm 19 | directory: /docs 20 | schedule: 21 | interval: daily 22 | 23 | - package-ecosystem: gomod 24 | directory: / 25 | schedule: 26 | interval: daily 27 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Sql/kql/4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all Elastic Pools that have no databases associated 3 | resources 4 | | where type =~ 'microsoft.sql/servers/elasticpools' 5 | | project elasticPoolId = tolower(id), name, Resource = id, resourceGroup, location, subscriptionId, tags, properties 6 | | join kind=leftouter (resources 7 | | where type =~ 'Microsoft.Sql/servers/databases' 8 | | project id, properties 9 | | extend elasticPoolId = tolower(properties.elasticPoolId)) on elasticPoolId 10 | | summarize databaseCount = countif(id != '') by Resource, name, resourceGroup, location, subscriptionId, tostring(tags) 11 | | where databaseCount == 0 12 | | project recommendationId="4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a", name, id=Resource, tags 13 | -------------------------------------------------------------------------------- /internal/scanners/netapp/netapp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package netapp 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["netapp"] = []models.IAzureScanner{NewNetAppScanner()} 12 | } 13 | 14 | // NewNetAppScanner creates a new NetAppScanner 15 | func NewNetAppScanner() *NetAppScanner { 16 | return &NetAppScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.NetApp/netAppAccounts"), 18 | } 19 | } 20 | 21 | // NetAppScanner - Scanner for NetApp 22 | type NetAppScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the NetApp Scanner 27 | func (a *NetAppScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /scripts/install.ps1: -------------------------------------------------------------------------------- 1 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 2 | $arch = "amd64" 3 | } elseif ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { 4 | $arch = "arm64" 5 | } else { 6 | Write-Host "Unsupported architecture: $($env:PROCESSOR_ARCHITECTURE)" 7 | exit 8 | } 9 | 10 | $latest_azqr=$(iwr https://api.github.com/repos/Azure/azqr/releases/latest).content | convertfrom-json | Select-Object -ExpandProperty tag_name 11 | iwr https://github.com/Azure/azqr/releases/download/$latest_azqr/azqr-win-$arch.zip -OutFile azqr.zip 12 | Expand-Archive -Path azqr.zip -DestinationPath ./azqr_bin 13 | Get-ChildItem -Path ./azqr_bin -Recurse -File | ForEach-Object { Move-Item -Path $_.FullName -Destination . -Force } 14 | Remove-Item -Path ./azqr_bin -Recurse -Force 15 | Remove-Item -Path azqr.zip 16 | .\azqr.exe --version -------------------------------------------------------------------------------- /internal/scanners/avs/avs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package avs 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["avs"] = []models.IAzureScanner{NewAVSScanner()} 12 | } 13 | 14 | // NewAVSScanner creates a new AVSScanner 15 | func NewAVSScanner() *AVSScanner { 16 | return &AVSScanner{ 17 | BaseScanner: models.NewBaseScanner( 18 | "Microsoft.AVS/privateClouds", 19 | "Specialized.Workload/AVS", 20 | ), 21 | } 22 | } 23 | 24 | // AVSScanner - Scanner for AVS 25 | type AVSScanner struct { 26 | models.BaseScanner 27 | } 28 | 29 | // Init - Initializes the AVS Scanner 30 | func (a *AVSScanner) Init(config *models.ScannerConfig) error { 31 | return a.BaseScanner.Init(config) 32 | } 33 | -------------------------------------------------------------------------------- /docs/go.sum: -------------------------------------------------------------------------------- 1 | github.com/FortAwesome/Font-Awesome v0.0.0-20230327165841-0698449d50f2/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo= 2 | github.com/FortAwesome/Font-Awesome v0.0.0-20240716171331-37eff7fa00de/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo= 3 | github.com/google/docsy v0.11.0 h1:QnV40cc28QwS++kP9qINtrIv4hlASruhC/K3FqkHAmM= 4 | github.com/google/docsy v0.11.0/go.mod h1:hGGW0OjNuG5ZbH5JRtALY3yvN8ybbEP/v2iaK4bwOUI= 5 | github.com/google/docsy/dependencies v0.7.2 h1:+t5ufoADQAj4XneFphz4A+UU0ICAxmNaRHVWtMYXPSI= 6 | github.com/google/docsy/dependencies v0.7.2/go.mod h1:gihhs5gmgeO+wuoay4FwOzob+jYJVyQbNaQOh788lD4= 7 | github.com/twbs/bootstrap v5.2.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0= 8 | github.com/twbs/bootstrap v5.3.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0= 9 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/4b5c6d7e-8f9a-0b1c-2d3e-4f5a6b7c8d9e.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all subnets without Connected Devices or Delegation 3 | resources 4 | | where type =~ "microsoft.network/virtualnetworks" 5 | | extend subnet = properties.subnets 6 | | mv-expand subnet 7 | | extend ipConfigurations = subnet.properties.ipConfigurations 8 | | extend delegations = subnet.properties.delegations 9 | | extend applicationGatewayIPConfigurations = subnet.properties.applicationGatewayIPConfigurations 10 | | where isnull(ipConfigurations) and delegations == "[]" and isnull(applicationGatewayIPConfigurations) 11 | | extend SubnetName = subnet.name, SubnetId = subnet.id 12 | | project recommendationId="4b5c6d7e-8f9a-0b1c-2d3e-4f5a6b7c8d9e", name=SubnetName, id=SubnetId, tags, param1=strcat("VNET Name: ", name) 13 | -------------------------------------------------------------------------------- /internal/scanners/conn/conn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package conn 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["conn"] = []models.IAzureScanner{NewConnectionScanner()} 12 | } 13 | 14 | // NewConnectionScanner creates a new ConnectionScanner 15 | func NewConnectionScanner() *ConnectionScanner { 16 | return &ConnectionScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Network/connections"), 18 | } 19 | } 20 | 21 | // ConnectionScanner - Scanner for Connection 22 | type ConnectionScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Connection Scanner 27 | func (a *ConnectionScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/ba/ba.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package ba 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["ba"] = []models.IAzureScanner{NewBatchAccountScanner()} 12 | } 13 | 14 | // NewBatchAccountScanner creates a new BatchAccountScanner 15 | func NewBatchAccountScanner() *BatchAccountScanner { 16 | return &BatchAccountScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Batch/batchAccounts"), 18 | } 19 | } 20 | 21 | // BatchAccountScanner - Scanner for Batch Account 22 | type BatchAccountScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Batch Account Scanner 27 | func (a *BatchAccountScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Compute/kql/3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all disks that are not attached 3 | resources 4 | | where type has "microsoft.compute/disks" 5 | | extend diskState = tostring(properties.diskState) 6 | | where (managedBy == "" and diskState != 'ActiveSAS') or (diskState == 'Unattached' and diskState != 'ActiveSAS') 7 | | where not(name endswith "-ASRReplica" or name startswith "ms-asr-" or name startswith "asrseeddisk-") 8 | | where (tags !contains "kubernetes.io-created-for-pvc") and tags !contains "ASR-ReplicaDisk" and tags !contains "asrseeddisk" and tags !contains "RSVaultBackup" 9 | | extend Details = pack_all() 10 | | project recommendationId="3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f", name, id, tags, param1=strcat("Sku: ", sku.name), param2=strcat("diskSizeGB: ", properties.diskSizeGB) 11 | -------------------------------------------------------------------------------- /internal/scanners/rg/rg.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package rg 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["rg"] = []models.IAzureScanner{NewResourceGroupScanner()} 12 | } 13 | 14 | // NewResourceGroupScanner creates a new ResourceGroupScanner 15 | func NewResourceGroupScanner() *ResourceGroupScanner { 16 | return &ResourceGroupScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Resources/resourceGroups"), 18 | } 19 | } 20 | 21 | // ResourceGroupScanner - Scanner for Resource Groups 22 | type ResourceGroupScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Resource Groups Scanner 27 | func (a *ResourceGroupScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/server/commands/root.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "os" 8 | "time" 9 | 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | var ( 17 | version = "dev" 18 | ) 19 | 20 | var rootCmd = &cobra.Command{ 21 | Use: "azqr-server", 22 | Short: "Azure Quick Review (azqr) API and MCP server", 23 | Long: "Azure Quick Review (azqr) API and MCP server", 24 | Args: cobra.NoArgs, 25 | Version: version, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | _ = cmd.Usage() 28 | }, 29 | } 30 | 31 | func Execute() { 32 | output := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339} 33 | 34 | log.Logger = zerolog.New(output).With().Timestamp().Logger() 35 | 36 | cobra.CheckErr(rootCmd.Execute()) 37 | } 38 | -------------------------------------------------------------------------------- /internal/scanners/rsv/rsv.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package rsv 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["rsv"] = []models.IAzureScanner{NewRecoveryServiceScanner()} 12 | } 13 | 14 | // NewRecoveryServiceScanner creates a new RecoveryServiceScanner 15 | func NewRecoveryServiceScanner() *RecoveryServiceScanner { 16 | return &RecoveryServiceScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.RecoveryServices/vaults"), 18 | } 19 | } 20 | 21 | // RecoveryServiceScanner - Scanner for Recovery Service 22 | type RecoveryServiceScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Recovery Service Scanner 27 | func (a *RecoveryServiceScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/avail/avail.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package avail 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["avail"] = []models.IAzureScanner{NewAvailabilitySetScanner()} 12 | } 13 | 14 | // NewAvailabilitySetScanner creates a new AvailabilitySetScanner 15 | func NewAvailabilitySetScanner() *AvailabilitySetScanner { 16 | return &AvailabilitySetScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Compute/availabilitySets"), 18 | } 19 | } 20 | 21 | // AvailabilitySetScanner - Scanner for Availability Set 22 | type AvailabilitySetScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Availability Set Scanner 27 | func (a *AvailabilitySetScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/aa/aa.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package aa 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["aa"] = []models.IAzureScanner{NewAutomationAccountScanner()} 12 | } 13 | 14 | // NewAutomationAccountScanner creates a new AutomationAccountScanner 15 | func NewAutomationAccountScanner() *AutomationAccountScanner { 16 | return &AutomationAccountScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Automation/automationAccounts"), 18 | } 19 | } 20 | 21 | // AutomationAccountScanner - Scanner for Automation Account 22 | type AutomationAccountScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Automation Account Scanner 27 | func (a *AutomationAccountScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/avd/avd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package avd 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["avd"] = []models.IAzureScanner{NewAzureVirtualDesktopScanner()} 12 | } 13 | 14 | // NewAzureVirtualDesktopScanner creates a new AzureVirtualDesktopScanner 15 | func NewAzureVirtualDesktopScanner() *AzureVirtualDesktopScanner { 16 | return &AzureVirtualDesktopScanner{ 17 | BaseScanner: models.NewBaseScanner("Specialized.Workload/AVD"), 18 | } 19 | } 20 | 21 | // AzureVirtualDesktopScanner - Scanner for Azure Virtual Desktop 22 | type AzureVirtualDesktopScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Azure Virtual Desktop Scanner 27 | func (a *AzureVirtualDesktopScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/erc/erc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package erc 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["erc"] = []models.IAzureScanner{NewExpressRouteScanner()} 12 | } 13 | 14 | // NewExpressRouteScanner creates a new ExpressRouteScanner 15 | func NewExpressRouteScanner() *ExpressRouteScanner { 16 | return &ExpressRouteScanner{ 17 | BaseScanner: models.NewBaseScanner( 18 | "Microsoft.Network/expressRouteCircuits", 19 | "Microsoft.Network/ExpressRoutePorts", 20 | ), 21 | } 22 | } 23 | 24 | // ExpressRouteScanner - Scanner for Express Route 25 | type ExpressRouteScanner struct { 26 | models.BaseScanner 27 | } 28 | 29 | // Init - Initializes the Express Route Scanner 30 | func (a *ExpressRouteScanner) Init(config *models.ScannerConfig) error { 31 | return a.BaseScanner.Init(config) 32 | } 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Web/kql/2d3e4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all API Connections not related to any Logic App 3 | resources 4 | | where type =~ 'Microsoft.Web/connections' 5 | | project subscriptionId, Resource = id , apiName = name, resourceGroup, tags, location 6 | | join kind = leftouter ( 7 | resources 8 | | where type == 'microsoft.logic/workflows' 9 | | extend resourceGroup, location, subscriptionId, properties 10 | | extend varJson = properties["parameters"]["$connections"]["value"] 11 | | mvexpand varConnection = varJson 12 | | where notnull(varConnection) 13 | | extend connectionId = extract("connectionId\":\"(.*?)\"", 1, tostring(varConnection)) 14 | | project connectionId, name 15 | ) 16 | on $left.Resource == $right.connectionId 17 | | where connectionId == "" 18 | | project recommendationId="2d3e4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a", name, id=Resource, tags 19 | -------------------------------------------------------------------------------- /internal/scanners/odb/odb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package odb 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["odb"] = []models.IAzureScanner{NewOracleDatabaseScanner()} 12 | } 13 | 14 | // NewOracleDatabaseScanner creates a new OracleDatabaseScanner 15 | func NewOracleDatabaseScanner() *OracleDatabaseScanner { 16 | return &OracleDatabaseScanner{ 17 | BaseScanner: models.NewBaseScanner( 18 | "Oracle.Database/cloudExadataInfrastructures", 19 | "Oracle.Database/cloudVmClusters", 20 | ), 21 | } 22 | } 23 | 24 | // OracleDatabaseScanner - Scanner for Oracle Database@Azure 25 | type OracleDatabaseScanner struct { 26 | models.BaseScanner 27 | } 28 | 29 | // Init - Initializes the Oracle Database Scanner 30 | func (a *OracleDatabaseScanner) Init(config *models.ScannerConfig) error { 31 | return a.BaseScanner.Init(config) 32 | } 33 | -------------------------------------------------------------------------------- /docs/content/en/docs/Install/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Install 3 | weight: 2 4 | description: Learn how to install Azure Quick Review (azqr) 5 | --- 6 | 7 | ## Install on Linux or Azure Cloud Shell 8 | 9 | ```bash 10 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/azure/azqr/main/scripts/install.sh)" 11 | ``` 12 | 13 | ## Install on Windows 14 | 15 | Use `winget`: 16 | 17 | ``` console 18 | winget install azqr 19 | ``` 20 | 21 | or download the executable file: 22 | 23 | ``` 24 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/azure/azqr/main/scripts/install.ps1')) 25 | ``` 26 | 27 | ## Install on Mac 28 | 29 | Use `homebrew`: 30 | 31 | ```console 32 | brew install azqr 33 | ``` 34 | 35 | or download the latest release from [here](https://github.com/Azure/azqr/releases). -------------------------------------------------------------------------------- /internal/scanners/fdfp/fdfp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package fdfp 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["fdfp"] = []models.IAzureScanner{NewFrontDoorWAFPolicyScanner()} 12 | } 13 | 14 | // NewFrontDoorWAFPolicyScanner creates a new FrontDoorWAFPolicyScanner 15 | func NewFrontDoorWAFPolicyScanner() *FrontDoorWAFPolicyScanner { 16 | return &FrontDoorWAFPolicyScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Network/frontdoorWebApplicationFirewallPolicies"), 18 | } 19 | } 20 | 21 | // FrontDoorWAFPolicyScanner - Scanner for Front Door Web Application Policy 22 | type FrontDoorWAFPolicyScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Front Door Web Application Policy Scanner 27 | func (a *FrontDoorWAFPolicyScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/hpc/hpc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package hpc 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["hpc"] = []models.IAzureScanner{NewHighPerformanceComputingScanner()} 12 | } 13 | 14 | // NewHighPerformanceComputingScanner creates a new HighPerformanceComputingScanner 15 | func NewHighPerformanceComputingScanner() *HighPerformanceComputingScanner { 16 | return &HighPerformanceComputingScanner{ 17 | BaseScanner: models.NewBaseScanner("Specialized.Workload/HPC"), 18 | } 19 | } 20 | 21 | // HighPerformanceComputingScanner - Scanner for High Performance Computing 22 | type HighPerformanceComputingScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the High Performance Computing Scanner 27 | func (a *HighPerformanceComputingScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | -------------------------------------------------------------------------------- /internal/scanners/pdnsz/pdnsz.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package pdnsz 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["pdnsz"] = []models.IAzureScanner{NewPrivateDNSZoneScanner()} 12 | } 13 | 14 | // NewPrivateDNSZoneScanner creates a new PrivateDNSZoneScanner 15 | func NewPrivateDNSZoneScanner() *PrivateDNSZoneScanner { 16 | return &PrivateDNSZoneScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.Network/privateDnsZones"), 18 | } 19 | } 20 | 21 | // PrivateDNSZoneScanner - Scanner for Private DNS Zone 22 | type PrivateDNSZoneScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Private DNS Zone Scanner 27 | func (a *PrivateDNSZoneScanner) Init(config *models.ScannerConfig) error { 28 | return a.BaseScanner.Init(config) 29 | } 30 | 31 | // TODO: version 6.1.0 of armentowrk does not allow listing per subscription yet. 32 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all virtual network gateways without Point-to-site configuration or Connections 3 | resources 4 | | where type =~ "microsoft.network/virtualnetworkgateways" 5 | | extend SKU = tostring(properties.sku.name) 6 | | extend Tier = tostring(properties.sku.tier) 7 | | extend GatewayType = tostring(properties.gatewayType) 8 | | extend vpnClientConfiguration = properties.vpnClientConfiguration 9 | | extend Resource = id 10 | | join kind=leftouter ( 11 | resources 12 | | where type =~ "microsoft.network/connections" 13 | | mv-expand Resource = pack_array(properties.virtualNetworkGateway1.id, properties.virtualNetworkGateway2.id) to typeof(string) 14 | | project Resource, connectionId = id, ConnectionProperties=properties 15 | ) on Resource 16 | | where isempty(vpnClientConfiguration) and isempty(connectionId) 17 | | project recommendationId="9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d", name, id, tags 18 | -------------------------------------------------------------------------------- /internal/scanners/vdpool/vdpool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package vdpool 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["vdpool"] = []models.IAzureScanner{NewVirtualDesktopScanner()} 12 | } 13 | 14 | // NewVirtualDesktopScanner creates a new VirtualDesktopScanner 15 | func NewVirtualDesktopScanner() *VirtualDesktopScanner { 16 | return &VirtualDesktopScanner{ 17 | BaseScanner: models.NewBaseScanner( 18 | "Microsoft.DesktopVirtualization/hostPools", 19 | "Microsoft.DesktopVirtualization/scalingPlans", 20 | "Microsoft.DesktopVirtualization/workspaces", 21 | ), 22 | } 23 | } 24 | 25 | // VirtualDesktopScanner - Scanner for Virtual Desktop 26 | type VirtualDesktopScanner struct { 27 | models.BaseScanner 28 | } 29 | 30 | // Init - Initializes the Virtual Desktop Scanner 31 | func (a *VirtualDesktopScanner) Init(config *models.ScannerConfig) error { 32 | return a.BaseScanner.Init(config) 33 | } 34 | -------------------------------------------------------------------------------- /examples/cicd/github-actions.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow for azqr 2 | 3 | name: azqr-pipeline 4 | 5 | on: 6 | workflow_dispatch: # Trigger manually 7 | schedule: # Trigger every Friday at midnight 8 | - cron: "0 0 * * 5" 9 | push: 10 | branches: 11 | - main # Trigger on push to main 12 | pull_request: 13 | branches: 14 | - main # Trigger on pull request to main 15 | 16 | jobs: 17 | azqr: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | # Checkout the repository 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | # Run azqr scan 26 | - name: Run Azure Quick Review 27 | uses: Azure/azqr/.github/actions/azqr-scan 28 | env: 29 | AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} 30 | AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} 31 | AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 32 | with: 33 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 34 | output-format: 'json' 35 | -------------------------------------------------------------------------------- /docs/content/en/docs/Troubleshooting/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Troubleshooting & Support 3 | description: Troubleshooting & Support 4 | weight: 4 5 | --- 6 | 7 | If you encounter any issue while using **Azure Quick Review (azqr)**, please set the `AZURE_SDK_GO_LOGGING` environment variable to `all`, run the tool with the `--debug` flag and then share the console output with us by filing a new [issue](https://github.com/Azure/azqr/issues). 8 | 9 | ## Support 10 | 11 | This project uses GitHub Issues to track bugs and feature requests. 12 | Before logging an issue please check our [troubleshooting](#troubleshooting) guide. 13 | 14 | Please search the existing issues before filing new issues to avoid duplicates. 15 | 16 | - For new issues, file your bug or feature request as a new [issue](https://github.com/Azure/azqr/issues). 17 | - For help, discussion, and support questions about using this project, join or start a [discussion](https://github.com/Azure/azqr/discussions). 18 | 19 | Support for this project / product is limited to the resources listed above. -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Scan", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "args": [ 14 | "scan" 15 | ], 16 | "env": { 17 | "AZURE_SDK_GO_LOGGING": "all" 18 | } 19 | }, 20 | { 21 | "name": "Launch Server", 22 | "type": "go", 23 | "request": "launch", 24 | "mode": "auto", 25 | "program": "${fileDirname}", 26 | "args": [ 27 | "serve" 28 | ], 29 | "env": { 30 | "AZURE_SDK_GO_LOGGING": "all" 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | coverage.txt 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Jetbrains IDE 19 | .idea/ 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | # Go workspace file 24 | go.work 25 | 26 | # Go profiling 27 | *.prof 28 | profiles/ 29 | 30 | ## Hugo & Node 31 | public/ 32 | resources/ 33 | node_modules/ 34 | package-lock.json 35 | .hugo_build.lock 36 | 37 | dist/ 38 | 39 | # azqr build and runtime artifacts 40 | .azqr 41 | main 42 | main.exe 43 | 44 | # azqr binaries 45 | bin/ 46 | /azqr 47 | azqr.exe 48 | 49 | # azqr outputs 50 | *.xlsx 51 | *.csv 52 | 53 | # Windows resource files (generated by make) 54 | cmd/azqr/winres/winres.json 55 | cmd/azqr/*.syso 56 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/renderers/excel/advisor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | func renderAdvisor(f *excelize.File, data *renderers.ReportData) { 15 | _, err := f.NewSheet("Advisor") 16 | if err != nil { 17 | log.Fatal().Err(err).Msg("Failed to create Advisor sheet") 18 | } 19 | 20 | records := data.AdvisorTable() 21 | headers := records[0] 22 | createFirstRow(f, "Advisor", headers) 23 | 24 | if len(data.Advisor) > 0 { 25 | records = records[1:] 26 | currentRow := 4 27 | for _, row := range records { 28 | currentRow += 1 29 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 30 | if err != nil { 31 | log.Fatal().Err(err).Msg("Failed to get cell") 32 | } 33 | err = f.SetSheetRow("Advisor", cell, &row) 34 | if err != nil { 35 | log.Fatal().Err(err).Msg("Failed to set row") 36 | } 37 | } 38 | 39 | configureSheet(f, "Advisor", headers, currentRow) 40 | } else { 41 | log.Info().Msg("Skipping Advisor. No data to render") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/renderers/excel/cost.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | func renderCosts(f *excelize.File, data *renderers.ReportData) { 15 | _, err := f.NewSheet("Costs") 16 | if err != nil { 17 | log.Fatal().Err(err).Msg("Failed to create Costs sheet") 18 | } 19 | 20 | records := data.CostTable() 21 | headers := records[0] 22 | createFirstRow(f, "Costs", headers) 23 | 24 | if data.Cost != nil && len(data.Cost.Items) > 0 { 25 | records = records[1:] 26 | currentRow := 4 27 | for _, row := range records { 28 | currentRow += 1 29 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 30 | if err != nil { 31 | log.Fatal().Err(err).Msg("Failed to get cell") 32 | } 33 | err = f.SetSheetRow("Costs", cell, &row) 34 | if err != nil { 35 | log.Fatal().Err(err).Msg("Failed to set row") 36 | } 37 | } 38 | 39 | configureSheet(f, "Costs", headers, currentRow) 40 | } else { 41 | log.Info().Msg("Skipping Costs. No data to render") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/throttling/limiter.go: -------------------------------------------------------------------------------- 1 | package throttling 2 | 3 | import ( 4 | "context" 5 | 6 | "golang.org/x/time/rate" 7 | ) 8 | 9 | // ARMLimiter rate limits Azure Resource Manager API calls 10 | // Allows 3 operations per second with burst capacity of 100 11 | // https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling#regional-throttling-and-token-bucket-algorithm 12 | var ARMLimiter = rate.NewLimiter(rate.Limit(3), 100) 13 | 14 | // GraphLimiter rate limits Azure Resource Graph API calls 15 | // Allows 3 operations per second with burst capacity of 10 16 | // With higher burst capacity to better utilize the 5-second window 17 | // https://learn.microsoft.com/en-us/azure/governance/resource-graph/concepts/guidance-for-throttled-requests#staggering-queries 18 | var GraphLimiter = rate.NewLimiter(rate.Limit(2), 10) 19 | 20 | // WaitARM waits for permission to make an ARM API call using the provided context 21 | func WaitARM(ctx context.Context) error { 22 | return ARMLimiter.Wait(ctx) 23 | } 24 | 25 | // WaitGraph waits for permission to make a Graph API call using the provided context 26 | func WaitGraph(ctx context.Context) error { 27 | return GraphLimiter.Wait(ctx) 28 | } 29 | -------------------------------------------------------------------------------- /cmd/azqr/commands/root.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package commands 5 | 6 | import ( 7 | "os" 8 | "time" 9 | 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/Azure/azqr/internal/plugins" 13 | "github.com/rs/zerolog" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | var ( 18 | version = "dev" 19 | ) 20 | 21 | var rootCmd = &cobra.Command{ 22 | Use: "azqr", 23 | Short: "Azure Quick Review (azqr) goal is to produce a high level assessment of an Azure Subscription or Resource Group", 24 | Long: `Azure Quick Review (azqr) goal is to produce a high level assessment of an Azure Subscription or Resource Group`, 25 | Args: cobra.NoArgs, 26 | Version: version, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | _ = cmd.Usage() 29 | }, 30 | } 31 | 32 | func Execute() { 33 | output := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339} 34 | 35 | log.Logger = zerolog.New(output).With().Timestamp().Logger() 36 | 37 | // Load all YAML plugins after logger is configured 38 | if err := plugins.LoadAll(); err != nil { 39 | log.Warn().Err(err).Msg("Failed to load some plugins") 40 | } 41 | 42 | cobra.CheckErr(rootCmd.Execute()) 43 | } 44 | -------------------------------------------------------------------------------- /internal/scanners/arc/arc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package arc 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | ) 9 | 10 | func init() { 11 | models.ScannerList["arc"] = []models.IAzureScanner{NewArcScanner()} 12 | } 13 | 14 | // NewArcScanner creates a new ArcScanner 15 | func NewArcScanner() *ArcScanner { 16 | return &ArcScanner{ 17 | BaseScanner: models.NewBaseScanner("Microsoft.AzureArcData/sqlServerInstances"), 18 | } 19 | } 20 | 21 | // ArcScanner - Scanner for Arc SQL 22 | type ArcScanner struct { 23 | models.BaseScanner 24 | } 25 | 26 | // Init - Initializes the Arc SQL Scanner 27 | func (c *ArcScanner) Init(config *models.ScannerConfig) error { 28 | return c.BaseScanner.Init(config) 29 | } 30 | 31 | // Scan - Scans all Azure Arc-enabled machines in a Resource Group 32 | func (c *ArcScanner) Scan(scanContext *models.ScanContext) ([]*models.AzqrServiceResult, error) { 33 | models.LogSubscriptionScan(c.BaseScanner.GetConfig().SubscriptionID, c.ResourceTypes()[0]) 34 | // This scanner doesn't perform actual scans - it's here to register the resource type 35 | // Actual Arc SQL scanning is done by the graph-based ArcSQLScanner 36 | return []*models.AzqrServiceResult{}, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/renderers/excel/arc_sql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | // renderArcSQL creates and populates the Arc SQL sheet in the Excel report. 15 | func renderArcSQL(f *excelize.File, data *renderers.ReportData) { 16 | _, err := f.NewSheet("Arc SQL") 17 | if err != nil { 18 | log.Fatal().Err(err).Msg("Failed to create Arc SQL sheet") 19 | } 20 | 21 | records := data.ArcSQLTable() 22 | headers := records[0] 23 | createFirstRow(f, "Arc SQL", headers) 24 | 25 | if len(data.ArcSQL) > 0 { 26 | records = records[1:] 27 | currentRow := 4 28 | for _, row := range records { 29 | currentRow += 1 30 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 31 | if err != nil { 32 | log.Fatal().Err(err).Msg("Failed to get cell") 33 | } 34 | err = f.SetSheetRow("Arc SQL", cell, &row) 35 | if err != nil { 36 | log.Fatal().Err(err).Msg("Failed to set row") 37 | } 38 | } 39 | 40 | configureSheet(f, "Arc SQL", headers, currentRow) 41 | } else { 42 | log.Info().Msg("Skipping Arc SQL. No data to render") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /jumpstart_drops/azqr-tutorial.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": " Azure Quick Review Tutorial", 3 | "Summary": "This Drop provides a tutorial on how to use the Azure Quick Review tool to assess your Azure environment.", 4 | "Description": "This Drop provides a tutorial on how to use the Azure Quick Review tool to assess your Azure environment. The Azure Quick Review tool is a free, open-source tool that helps you assess your Azure environment and identify areas for improvement. The tool provides a comprehensive report that includes recommendations for best practices, security, and cost optimization.", 5 | "Cover": "", 6 | "Authors": [ 7 | { 8 | "Name": "Carlos Mendible", 9 | "Link": "https://linkedin.com/in/carlosmendible" 10 | } 11 | ], 12 | "Source": "https://github.com/Azure/azqr/tree/main/jumpstart_drops/azqr-tutorial", 13 | "Type": "tutorial_guide", 14 | "Difficulty": "Medium", 15 | "ProgrammingLanguage": [ 16 | "Bash", 17 | "Powershell" 18 | ], 19 | "Products": [ 20 | "Azure Kubernetes Service", 21 | "Azure Cosmos DB", 22 | "Azure Firewall" 23 | ], 24 | "LastModified": "2025-02-25T14:27:00.000Z", 25 | "CreatedDate": "2025-02-25T14:27:00.000Z", 26 | "Topics": [] 27 | } 28 | -------------------------------------------------------------------------------- /docs/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /internal/embeded/embeded_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package embeded 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestGetTemplates_ValidFile(t *testing.T) { 11 | // Test reading the embedded azqr.png file 12 | data := GetTemplates("azqr.png") 13 | 14 | if data == nil { 15 | t.Error("GetTemplates() returned nil for valid file azqr.png") 16 | } 17 | 18 | if len(data) == 0 { 19 | t.Error("GetTemplates() returned empty data for azqr.png") 20 | } 21 | 22 | // PNG files start with specific magic bytes 23 | if len(data) >= 4 { 24 | // PNG magic bytes: 89 50 4E 47 25 | if data[0] != 0x89 || data[1] != 0x50 || data[2] != 0x4E || data[3] != 0x47 { 26 | t.Error("GetTemplates() did not return valid PNG data") 27 | } 28 | } 29 | } 30 | 31 | func TestGetTemplates_InvalidFile(t *testing.T) { 32 | // Test reading a non-existent file 33 | data := GetTemplates("nonexistent.png") 34 | 35 | if data != nil { 36 | t.Error("GetTemplates() should return nil for non-existent file") 37 | } 38 | } 39 | 40 | func TestGetTemplates_EmptyFilename(t *testing.T) { 41 | // Test with empty filename 42 | data := GetTemplates("") 43 | 44 | if data != nil { 45 | t.Error("GetTemplates() should return nil for empty filename") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/renderers/excel/impacted.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | func renderImpactedResources(f *excelize.File, data *renderers.ReportData) { 15 | sheetName := "ImpactedResources" 16 | _, err := f.NewSheet(sheetName) 17 | if err != nil { 18 | log.Fatal().Err(err).Msg("Failed to create APRL sheet") 19 | } 20 | 21 | records := data.ImpactedTable() 22 | headers := records[0] 23 | createFirstRow(f, sheetName, headers) 24 | 25 | if len(records) > 0 { 26 | records = records[1:] 27 | currentRow := 4 28 | for _, row := range records { 29 | currentRow += 1 30 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 31 | if err != nil { 32 | log.Fatal().Err(err).Msg("Failed to get cell") 33 | } 34 | err = f.SetSheetRow(sheetName, cell, &row) 35 | if err != nil { 36 | log.Fatal().Err(err).Msg("Failed to set row") 37 | } 38 | setHyperLink(f, sheetName, 18, currentRow) 39 | } 40 | 41 | configureSheet(f, sheetName, headers, currentRow) 42 | } else { 43 | log.Info().Msgf("Skipping %s. No data to render", sheetName) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/scanners/avs/avs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package avs 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestAVSScanner_Init(t *testing.T) { 13 | scanner := NewAVSScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestAVSScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewAVSScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestAVSScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewAVSScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/scanners/nic/nic_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package nic 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestNICScanner_Init(t *testing.T) { 13 | scanner := NewNICScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestNICScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewNICScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestNICScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewNICScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/scanners/iot/iot_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package iot 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestIoTHubScanner_Init(t *testing.T) { 13 | scanner := NewIoTHubScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestIoTHubScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewIoTHubScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestIoTHubScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewIoTHubScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/scanners/netapp/netapp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package netapp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestNetAppScanner_Init(t *testing.T) { 13 | scanner := NewNetAppScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestNetAppScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewNetAppScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestNetAppScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewNetAppScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/renderers/excel/azure_policy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | // renderAzurePolicy creates and populates the Azure Policy sheet in the Excel report. 15 | func renderAzurePolicy(f *excelize.File, data *renderers.ReportData) { 16 | _, err := f.NewSheet("Azure Policy") 17 | if err != nil { 18 | log.Fatal().Err(err).Msg("Failed to create Azure Policy sheet") 19 | } 20 | 21 | records := data.AzurePolicyTable() 22 | headers := records[0] 23 | createFirstRow(f, "Azure Policy", headers) 24 | 25 | if len(data.AzurePolicy) > 0 { 26 | records = records[1:] 27 | currentRow := 4 28 | for _, row := range records { 29 | currentRow += 1 30 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 31 | if err != nil { 32 | log.Fatal().Err(err).Msg("Failed to get cell") 33 | } 34 | err = f.SetSheetRow("Azure Policy", cell, &row) 35 | if err != nil { 36 | log.Fatal().Err(err).Msg("Failed to set row") 37 | } 38 | } 39 | 40 | configureSheet(f, "Azure Policy", headers, currentRow) 41 | } else { 42 | log.Info().Msg("Skipping Azure Policy. No data to render") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/renderers/excel/resourceTypes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | func renderResourceTypes(f *excelize.File, data *renderers.ReportData) { 15 | sheetName := "ResourceTypes" 16 | _, err := f.NewSheet(sheetName) 17 | if err != nil { 18 | log.Fatal().Err(err).Msgf("Failed to create %s sheet", sheetName) 19 | } 20 | 21 | records := data.ResourceTypesTable() 22 | headers := records[0] 23 | createFirstRow(f, sheetName, headers) 24 | 25 | if len(data.ResourceTypeCount) > 0 { 26 | records = records[1:] 27 | 28 | currentRow := 4 29 | for _, row := range records { 30 | currentRow += 1 31 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 32 | if err != nil { 33 | log.Fatal().Err(err).Msg("Failed to get cell") 34 | } 35 | err = f.SetSheetRow(sheetName, cell, &row) 36 | if err != nil { 37 | log.Fatal().Err(err).Msg("Failed to set row") 38 | } 39 | // setHyperLink(f, sheetName, 12, currentRow) 40 | } 41 | 42 | configureSheet(f, sheetName, headers, currentRow) 43 | } else { 44 | log.Info().Msgf("Skipping %s. No data to render", sheetName) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/renderers/excel/recommendations.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | func renderRecommendations(f *excelize.File, data *renderers.ReportData) { 15 | sheetName := "Recommendations" 16 | err := f.SetSheetName("Sheet1", sheetName) 17 | if err != nil { 18 | log.Fatal().Err(err).Msgf("Failed to create %s sheet", sheetName) 19 | } 20 | 21 | records := data.RecommendationsTable() 22 | headers := records[0] 23 | createFirstRow(f, sheetName, headers) 24 | 25 | if len(data.Recommendations) > 0 { 26 | records = records[1:] 27 | currentRow := 4 28 | for _, row := range records { 29 | currentRow += 1 30 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 31 | if err != nil { 32 | log.Fatal().Err(err).Msg("Failed to get cell") 33 | } 34 | err = f.SetSheetRow(sheetName, cell, &row) 35 | if err != nil { 36 | log.Fatal().Err(err).Msg("Failed to set row") 37 | } 38 | setHyperLink(f, sheetName, 11, currentRow) 39 | } 40 | 41 | configureSheet(f, sheetName, headers, currentRow) 42 | } else { 43 | log.Info().Msgf("Skipping %s. No data to render", sheetName) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/scanners/conn/conn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package conn 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestConnectionScanner_Init(t *testing.T) { 13 | scanner := NewConnectionScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestConnectionScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewConnectionScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestConnectionScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewConnectionScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/scanners/erc/erc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package erc 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestExpressRouteScanner_Init(t *testing.T) { 13 | scanner := NewExpressRouteScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestExpressRouteScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewExpressRouteScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestExpressRouteScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewExpressRouteScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/scanners/ba/ba_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package ba 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestBatchAccountScanner_Init(t *testing.T) { 13 | scanner := NewBatchAccountScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestBatchAccountScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewBatchAccountScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestBatchAccountScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewBatchAccountScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/scanners/rg/rg_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package rg 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestResourceGroupScanner_Init(t *testing.T) { 13 | scanner := NewResourceGroupScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestResourceGroupScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewResourceGroupScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestResourceGroupScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewResourceGroupScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/scanners/odb/odb_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package odb 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestOracleDatabaseScanner_Init(t *testing.T) { 13 | scanner := NewOracleDatabaseScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestOracleDatabaseScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewOracleDatabaseScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestOracleDatabaseScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewOracleDatabaseScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/scanners/rsv/rsv_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package rsv 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestRecoveryServiceScanner_Init(t *testing.T) { 13 | scanner := NewRecoveryServiceScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestRecoveryServiceScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewRecoveryServiceScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestRecoveryServiceScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewRecoveryServiceScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/scanners/pdnsz/pdnsz_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package pdnsz 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestPrivateDNSZoneScanner_Init(t *testing.T) { 13 | scanner := NewPrivateDNSZoneScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestPrivateDNSZoneScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewPrivateDNSZoneScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestPrivateDNSZoneScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewPrivateDNSZoneScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/throttling/policy.go: -------------------------------------------------------------------------------- 1 | package throttling 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 9 | ) 10 | 11 | // ThrottlingPolicy implements policy.Policy to apply rate limiting 12 | type ThrottlingPolicy struct{} 13 | 14 | // NewThrottlingPolicy creates a new throttling policy 15 | func NewThrottlingPolicy() policy.Policy { 16 | return &ThrottlingPolicy{} 17 | } 18 | 19 | // Do implements the policy.Policy interface 20 | func (p *ThrottlingPolicy) Do(req *policy.Request) (*http.Response, error) { 21 | // Apply rate limiting based on URL before sending request 22 | url := req.Raw().URL.String() 23 | if strings.Contains(url, "prices.azure.com") { 24 | if err := WaitGraph(req.Raw().Context()); err != nil { 25 | return nil, fmt.Errorf("throttling wait failed: %w", err) 26 | } 27 | } else if strings.Contains(url, "Microsoft.ResourceGraph/resources") { 28 | // Azure Resource Graph API has stricter rate limits 29 | if err := WaitGraph(req.Raw().Context()); err != nil { 30 | return nil, fmt.Errorf("throttling wait failed: %w", err) 31 | } 32 | } else { // Default to ARM throttling 33 | if err := WaitARM(req.Raw().Context()); err != nil { 34 | return nil, fmt.Errorf("throttling wait failed: %w", err) 35 | } 36 | } 37 | 38 | // Forward to next policy in pipeline 39 | return req.Next() 40 | } -------------------------------------------------------------------------------- /internal/scanners/avail/avail_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package avail 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestAvailabilitySetScanner_Init(t *testing.T) { 13 | scanner := NewAvailabilitySetScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestAvailabilitySetScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewAvailabilitySetScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestAvailabilitySetScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewAvailabilitySetScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /internal/scanners/fdfp/fdfp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package fdfp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestFrontDoorWAFPolicyScanner_Init(t *testing.T) { 13 | scanner := NewFrontDoorWAFPolicyScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestFrontDoorWAFPolicyScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewFrontDoorWAFPolicyScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestFrontDoorWAFPolicyScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewFrontDoorWAFPolicyScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/scanners/avd/avd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package avd 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestAzureVirtualDesktopScanner_Init(t *testing.T) { 13 | scanner := NewAzureVirtualDesktopScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestAzureVirtualDesktopScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewAzureVirtualDesktopScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestAzureVirtualDesktopScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewAzureVirtualDesktopScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/content/en/docs/Reference/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resources & References 3 | description: Check out other resources and references 4 | weight: 6 5 | --- 6 | 7 | ## Microsoft Documentation 8 | 9 | - [Azure Well-Architected Framework](https://docs.microsoft.com/en-us/azure/architecture/framework/) 10 | - [Azure Availability Zones](https://docs.microsoft.com/en-us/azure/availability-zones/az-overview) 11 | - [Azure Service Level Agreements](https://azure.microsoft.com/en-us/support/legal/sla/) 12 | - [Azure Diagnostic Settings](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-settings) 13 | - [Azure Private Link](https://docs.microsoft.com/en-us/azure/private-link/private-link-overview) 14 | - [Azure Advisor](https://docs.microsoft.com/en-us/azure/advisor/advisor-overview) 15 | - [Microsoft Defender for Cloud](https://learn.microsoft.com/en-us/azure/defender-for-cloud/defender-for-cloud-introduction) 16 | - [Azure Cost Management + Billing](https://docs.microsoft.com/en-us/azure/cost-management-billing/cost-management-billing-overview) 17 | 18 | ## Related Projects 19 | - [Azure Proactive Resiliency Library (APRL)](https://azure.github.io/Azure-Proactive-Resiliency-Library/) 20 | - [Azure Orphan Resources](https://github.com/dolevshor/azure-orphan-resources) 21 | - [review-checklist](https://github.com/Azure/review-checklists) 22 | - [PSRule.Rules.Azure](https://github.com/Azure/PSRule.Rules.Azure) 23 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Compute/queries.yaml: -------------------------------------------------------------------------------- 1 | - description: Availability Sets not associated to any VM or VMSS 2 | aprlGuid: 2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e 3 | recommendationTypeId: null 4 | recommendationControl: Governance 5 | recommendationImpact: Medium 6 | recommendationResourceType: Microsoft.Compute/availabilitySets 7 | recommendationMetadataState: Active 8 | longDescription: | 9 | Availability Sets not associated to any VM or VMSS. 10 | potentialBenefits: Identifies unused resources 11 | pgVerified: false 12 | automationAvailable: false 13 | tags: [] 14 | learnMoreLink: 15 | - name: Availability Sets 16 | url: "https://learn.microsoft.com/en-us/azure/virtual-machines/availability-set-overview" 17 | 18 | - description: Managed Disks with 'Unattached' state 19 | aprlGuid: 3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f 20 | recommendationTypeId: null 21 | recommendationControl: Governance 22 | recommendationImpact: Medium 23 | recommendationResourceType: Microsoft.Compute/disks 24 | recommendationMetadataState: Active 25 | longDescription: | 26 | Managed Disks with 'Unattached' state. 27 | potentialBenefits: Identifies unused resources 28 | pgVerified: false 29 | automationAvailable: false 30 | tags: [] 31 | learnMoreLink: 32 | - name: Managed Disks 33 | url: "https://learn.microsoft.com/en-us/azure/virtual-machines/managed-disks-overview" 34 | -------------------------------------------------------------------------------- /internal/scanners/hpc/hpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package hpc 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestHighPerformanceComputingScanner_Init(t *testing.T) { 13 | scanner := NewHighPerformanceComputingScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestHighPerformanceComputingScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewHighPerformanceComputingScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestHighPerformanceComputingScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewHighPerformanceComputingScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all private endpoints not connected to any resource 3 | resources 4 | | where type =~ "microsoft.network/privateendpoints" 5 | | extend connection = iff(array_length(properties.manualPrivateLinkServiceConnections) > 0, properties.manualPrivateLinkServiceConnections[0], properties.privateLinkServiceConnections[0]) 6 | | extend subnetId = properties.subnet.id 7 | | extend subnetName = tostring(split(subnetId, "/")[-1]) 8 | | extend subnetIdSplit = split(subnetId, "/") 9 | | extend vnetId = strcat_array(array_slice(subnetIdSplit,0,8), "/") 10 | | extend vnetName = tostring(split(vnetId, "/")[-1]) 11 | | extend serviceId = tostring(connection.properties.privateLinkServiceId) 12 | | extend serviceIdSplit = split(serviceId, "/") 13 | | extend serviceName = tostring(serviceIdSplit[8]) 14 | | extend serviceTypeEnum = iff(isnotnull(serviceIdSplit[6]), tolower(strcat(serviceIdSplit[6], "/", serviceIdSplit[7])), "microsoft.network/privatelinkservices") 15 | | extend stateEnum = tostring(connection.properties.privateLinkServiceConnectionState.status) 16 | | extend groupIds = tostring(connection.properties.groupIds[0]) 17 | | where stateEnum == "Disconnected" 18 | | project recommendationId="8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", name, id, tags, param1=strcat("VNET Name: ", vnetName), param2=strcat("Subnet Name: ", subnetName) 19 | -------------------------------------------------------------------------------- /.github/workflows/bump-winget.yml: -------------------------------------------------------------------------------- 1 | name: Bump azqr version on winget 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'SemVer format release tag, i.e. 0.24.5' 8 | required: true 9 | repository_dispatch: 10 | types: [ bump-winget ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | winget-bump: 17 | name: Bump azqr winget 18 | runs-on: windows-latest 19 | defaults: 20 | run: 21 | shell: powershell 22 | steps: 23 | - name: Get version 24 | id: get_version 25 | run: | 26 | $version="" 27 | if ("${{ github.event_name }}" -eq "repository_dispatch") 28 | { 29 | $version="${{ github.event.client_payload.version }}" 30 | } 31 | else 32 | { 33 | $version="${{ github.event.inputs.version }}" 34 | } 35 | echo "WINGETVERSION=$($version.replace('v.',''))" >> $env:GITHUB_ENV 36 | - name: Create winget PR 37 | run: | 38 | iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe 39 | .\wingetcreate.exe update Microsoft.Azure.QuickReview -u $Env:URL -v ${{ env.WINGETVERSION }} -t $Env:TOKEN --submit 40 | env: 41 | TOKEN: ${{ secrets.WINGET_PAT_ACCESS_TOKEN }} 42 | URL: ${{ format('https://github.com/Azure/azqr/releases/download/v.{0}/azqr-win-amd64.zip', env.WINGETVERSION) }} 43 | -------------------------------------------------------------------------------- /internal/plugins/loader.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package plugins 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | // getPluginDirs returns the directories to search for YAML plugins 14 | func getPluginDirs() []string { 15 | home, _ := os.UserHomeDir() 16 | return []string{ 17 | filepath.Join(home, ".azqr", "plugins"), 18 | "./plugins", 19 | } 20 | } 21 | 22 | // LoadAll discovers and loads all available plugins (internal and YAML) 23 | func LoadAll() error { 24 | registry := GetRegistry() 25 | 26 | // Discover internal plugins (registered at init time) 27 | internalPlugins := discoverInternalPlugins() 28 | for _, plugin := range internalPlugins { 29 | if err := registry.Register(plugin); err != nil { 30 | log.Warn(). 31 | Err(err). 32 | Str("plugin", plugin.Metadata.Name). 33 | Msg("Failed to register internal plugin") 34 | } 35 | } 36 | 37 | // Discover YAML plugins 38 | yamlPlugins, err := discoverYamlPlugins(getPluginDirs()) 39 | if err != nil { 40 | log.Warn().Err(err).Msg("Failed to discover YAML plugins") 41 | } else { 42 | for _, plugin := range yamlPlugins { 43 | if err := registry.Register(plugin); err != nil { 44 | log.Warn(). 45 | Err(err). 46 | Str("plugin", plugin.Metadata.Name). 47 | Msg("Failed to register YAML plugin") 48 | } 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/models/base_scanner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package models 5 | 6 | // BaseScanner provides common implementation for scanners with minimal or no recommendations 7 | type BaseScanner struct { 8 | config *ScannerConfig 9 | resourceTypes []string 10 | } 11 | 12 | // NewBaseScanner creates a new base scanner with specified resource types 13 | func NewBaseScanner(resourceTypes ...string) BaseScanner { 14 | return BaseScanner{ 15 | resourceTypes: resourceTypes, 16 | } 17 | } 18 | 19 | // Init implements IAzureScanner.Init 20 | func (b *BaseScanner) Init(config *ScannerConfig) error { 21 | b.config = config 22 | return nil 23 | } 24 | 25 | // Scan implements IAzureScanner.Scan - returns empty results 26 | func (b *BaseScanner) Scan(scanContext *ScanContext) ([]*AzqrServiceResult, error) { 27 | LogSubscriptionScan(b.config.SubscriptionID, b.resourceTypes[0]) 28 | return []*AzqrServiceResult{}, nil 29 | } 30 | 31 | // ResourceTypes implements IAzureScanner.ResourceTypes 32 | func (b *BaseScanner) ResourceTypes() []string { 33 | return b.resourceTypes 34 | } 35 | 36 | // GetRecommendations implements IAzureScanner.GetRecommendations - returns empty map 37 | func (b *BaseScanner) GetRecommendations() map[string]AzqrRecommendation { 38 | return map[string]AzqrRecommendation{} 39 | } 40 | 41 | // GetConfig returns the scanner configuration 42 | func (b *BaseScanner) GetConfig() *ScannerConfig { 43 | return b.config 44 | } 45 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "Go", 5 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 6 | // Features to add to the dev container. More info: https://containers.dev/features. 7 | // "features": {}, 8 | // Configure tool-specific properties. 9 | "customizations": { 10 | // Configure properties specific to VS Code. 11 | "vscode": { 12 | // Set *default* container specific settings.json values on container create. 13 | "settings": { 14 | "go.useLanguageServer": true, 15 | "scm.defaultViewMode": "tree", 16 | "editor.formatOnSave": true, 17 | "githubPullRequests.createOnPublishBranch": "never" 18 | }, 19 | "extensions": [ 20 | "golang.go", 21 | "GrapeCity.gc-excelviewer", 22 | "GitHub.copilot", 23 | "mhutchie.git-graph" 24 | ] 25 | } 26 | }, 27 | "features": { 28 | "ghcr.io/devcontainers/features/azure-cli:1": {}, 29 | "ghcr.io/guiyomh/features/golangci-lint:0": {}, 30 | "ghcr.io/devcontainers/features/go:1": {} 31 | } 32 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 33 | // "forwardPorts": [], 34 | // Use 'postCreateCommand' to run commands after the container is created. 35 | // "postCreateCommand": "go version", 36 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 37 | // "remoteUser": "root" 38 | } -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Network/kql/2f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c.kql: -------------------------------------------------------------------------------- 1 | // Azure Resource Graph Query 2 | // Get all Application Gateways without backend 3 | resources 4 | | where type =~ 'microsoft.network/applicationgateways' 5 | | extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools , AppGwId = tostring(id) 6 | | project AppGwId, resourceGroup, location, subscriptionId, tags, name, SKUName, SKUTier, SKUCapacity 7 | | join ( 8 | resources 9 | | where type =~ 'microsoft.network/applicationgateways' 10 | | mvexpand backendPools = properties.backendAddressPools 11 | | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations) 12 | | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) 13 | | extend backendPoolName = backendPools.properties.backendAddressPools.name 14 | | extend AppGwId = tostring(id) 15 | | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by AppGwId 16 | ) on AppGwId 17 | | project-away AppGwId1 18 | | where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount)) 19 | | project recommendationId="2f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c", name, id=AppGwId, tags, param1=strcat("SKUTier: ", SKUTier), param2=strcat("SKUCapacity: ", SKUCapacity) 20 | -------------------------------------------------------------------------------- /internal/scanners/disk/disk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package disk 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestDiskScanner_Init(t *testing.T) { 13 | scanner := NewDiskScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestDiskScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewDiskScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | expectedTypes := []string{"Microsoft.Compute/disks"} 33 | if len(resourceTypes) != len(expectedTypes) { 34 | t.Errorf("ResourceTypes() returned %d types, want %d", len(resourceTypes), len(expectedTypes)) 35 | } 36 | 37 | if resourceTypes[0] != expectedTypes[0] { 38 | t.Errorf("ResourceTypes() = %v, want %v", resourceTypes, expectedTypes) 39 | } 40 | } 41 | 42 | func TestDiskScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewDiskScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | 50 | if len(recommendations) != 0 { 51 | t.Errorf("GetRecommendations() returned %d recommendations, expected 0", len(recommendations)) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /internal/scanners/arc/arc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package arc 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestArcScanner_Init(t *testing.T) { 13 | scanner := NewArcScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestArcScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewArcScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | expectedTypes := []string{"Microsoft.AzureArcData/sqlServerInstances"} 33 | if len(resourceTypes) != len(expectedTypes) { 34 | t.Errorf("ResourceTypes() returned %d types, want %d", len(resourceTypes), len(expectedTypes)) 35 | } 36 | 37 | if resourceTypes[0] != expectedTypes[0] { 38 | t.Errorf("ResourceTypes() = %v, want %v", resourceTypes, expectedTypes) 39 | } 40 | } 41 | 42 | func TestArcScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewArcScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | 50 | if len(recommendations) != 0 { 51 | t.Errorf("GetRecommendations() returned %d recommendations, expected 0", len(recommendations)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/renderers/excel/resources.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package excel 5 | 6 | import ( 7 | _ "image/png" 8 | 9 | "github.com/Azure/azqr/internal/renderers" 10 | "github.com/rs/zerolog/log" 11 | "github.com/xuri/excelize/v2" 12 | ) 13 | 14 | func renderResources(f *excelize.File, data *renderers.ReportData) { 15 | createResourcesSheet(f, "Inventory", data.ResourcesTable()) 16 | } 17 | 18 | func renderExcludedResources(f *excelize.File, data *renderers.ReportData) { 19 | createResourcesSheet(f, "OutOfScope", data.ExcludedResourcesTable()) 20 | } 21 | 22 | func createResourcesSheet(f *excelize.File, sheetName string, table [][]string) { 23 | _, err := f.NewSheet(sheetName) 24 | if err != nil { 25 | log.Fatal().Err(err).Msgf("Failed to create %s sheet", sheetName) 26 | } 27 | 28 | records := table 29 | headers := records[0] 30 | createFirstRow(f, sheetName, headers) 31 | 32 | if len(table) > 0 { 33 | records = records[1:] 34 | currentRow := 4 35 | for _, row := range records { 36 | currentRow += 1 37 | cell, err := excelize.CoordinatesToCellName(1, currentRow) 38 | if err != nil { 39 | log.Fatal().Err(err).Msg("Failed to get cell") 40 | } 41 | err = f.SetSheetRow(sheetName, cell, &row) 42 | if err != nil { 43 | log.Fatal().Err(err).Msg("Failed to set row") 44 | } 45 | setHyperLink(f, sheetName, 12, currentRow) 46 | } 47 | 48 | configureSheet(f, sheetName, headers, currentRow) 49 | } else { 50 | log.Info().Msg("Skipping Services. No data to render") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/test_graph_queries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to run KQL queries using Azure CLI 4 | # Arguments: 5 | # $1 - The KQL query to run 6 | runKqlQuery() { 7 | local kqlQuery="$1" 8 | echo "Running query: $kqlQuery" 9 | # Run the KQL query using the Azure CLI and stop on error 10 | az graph query -q "$kqlQuery" || { echo "Error running query: $kqlQuery"; exit 1; } 11 | } 12 | 13 | # Find all .kql files in the specified directory 14 | kqlFiles=$(find internal/graph/azure-orphan-resources -type f -name "*.kql") 15 | 16 | # Find all .kql files in the internal/graph/aprl/azure-resources directory and append them to kqlFiles 17 | aprlKqlFiles=$(find internal/graph/aprl/azure-resources -type f -name "*.kql") 18 | 19 | # Combine kqlFiles and aprlKqlFiles into a single variable 20 | kqlFiles="$kqlFiles $aprlKqlFiles" 21 | 22 | # Loop through each .kql file 23 | for kqlFile in $kqlFiles; do 24 | echo "Processing file: $kqlFile" 25 | 26 | # Read the entire content of the .kql file 27 | kqlQuery=$(<"$kqlFile") 28 | 29 | # dot not attempt to run th equery if it contains 30 | # "cannot-be-validated-with-arg, "under-development" or under development" 31 | if [[ "$kqlQuery" == *"cannot-be-validated-with-arg"* ]] || \ 32 | [[ "$kqlQuery" == *"under-development"* ]] || \ 33 | [[ "$kqlQuery" == *"under development"* ]]; then 34 | echo "Skipping query in $kqlFile due to manual validation or development status." 35 | continue 36 | fi 37 | 38 | # Run the KQL query using the Azure CLI 39 | runKqlQuery "$kqlQuery" 40 | done 41 | -------------------------------------------------------------------------------- /internal/scanners/gal/gal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package gal 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestGalleryScanner_Init(t *testing.T) { 13 | scanner := NewGalleryScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestGalleryScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewGalleryScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | expectedTypes := []string{"Microsoft.Compute/galleries"} 33 | if len(resourceTypes) != len(expectedTypes) { 34 | t.Errorf("ResourceTypes() returned %d types, want %d", len(resourceTypes), len(expectedTypes)) 35 | } 36 | 37 | if resourceTypes[0] != expectedTypes[0] { 38 | t.Errorf("ResourceTypes() = %v, want %v", resourceTypes, expectedTypes) 39 | } 40 | } 41 | 42 | func TestGalleryScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewGalleryScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | 50 | if len(recommendations) != 0 { 51 | t.Errorf("GetRecommendations() returned %d recommendations, expected 0", len(recommendations)) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /SECURITY_VERIFICATION.md: -------------------------------------------------------------------------------- 1 | # Binary Verification 2 | 3 | This document provides guidance on verifying the authenticity of Azure Quick Review (azqr) binaries. 4 | 5 | ## Checksum Verification 6 | 7 | Each release includes SHA256 checksums. Verify your download: 8 | 9 | ### Using our verification script (recommended) 10 | 11 | ```bash 12 | # Download and run the verification script 13 | curl -sL https://raw.githubusercontent.com/Azure/azqr/main/scripts/verify-checksum.sh -o verify-checksum.sh 14 | chmod +x verify-checksum.sh 15 | ./verify-checksum.sh 2.7.3 win-amd64 16 | ``` 17 | 18 | ### Manual verification 19 | 20 | ```bash 21 | # Download the checksum file 22 | curl -sL https://github.com/Azure/azqr/releases/download/v./azqr-win-amd64.zip.sha256 -o azqr-win-amd64.zip.sha256 23 | 24 | # Verify the checksum (Windows) 25 | CertUtil -hashfile azqr-win-amd64.zip SHA256 26 | 27 | # Verify the checksum (Linux/macOS) 28 | sha256sum -c azqr-win-amd64.zip.sha256 29 | ``` 30 | 31 | ## Download Source 32 | 33 | Only download from the official [GitHub releases page](https://github.com/Azure/azqr/releases). 34 | 35 | ## Release Integrity 36 | 37 | Check that the release is signed by Azure/azqr maintainers on GitHub. 38 | 39 | ## Build Integrity 40 | 41 | All binaries are built using GitHub Actions with: 42 | - Reproducible build environment 43 | - Pinned dependencies 44 | - Public build logs 45 | - Automated testing 46 | 47 | You can verify the build process by: 48 | 1. Checking the [build workflow](.github/workflows/build.yml) 49 | 2. Reviewing the [build logs](https://github.com/Azure/azqr/actions/workflows/build.yml) 50 | 3. Comparing source code with the tagged release -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azqr", 3 | "version": "0.7.1", 4 | "description": "Azure Quick Review documentation.", 5 | "repository": "github:azure/azqr", 6 | "homepage": "https://azure.github.io/azqr/", 7 | "author": "azqr Authors", 8 | "license": "Apache-2.0", 9 | "bugs": "https://github.com/azure/azqr/issues", 10 | "spelling": "cSpell:ignore HTMLTEST precheck postbuild -", 11 | "scripts": { 12 | "_build": "npm run _hugo-dev", 13 | "_check:links": "echo IMPLEMENTATION PENDING for check-links; echo", 14 | "_hugo": "hugo --cleanDestinationDir", 15 | "_hugo-dev": "npm run _hugo -- -e dev -DFE", 16 | "_serve": "npm run _hugo-dev -- --minify serve", 17 | "build:preview": "npm run _hugo-dev -- --minify --baseURL \"${DEPLOY_PRIME_URL:-/}\"", 18 | "build:production": "npm run _hugo -- --minify", 19 | "build": "npm run _build", 20 | "check:links:all": "HTMLTEST_ARGS= npm run _check:links", 21 | "check:links": "npm run _check:links", 22 | "clean": "rm -Rf public/* resources", 23 | "make:public": "git init -b main public", 24 | "precheck:links:all": "npm run build", 25 | "precheck:links": "npm run build", 26 | "postbuild:preview": "npm run _check:links", 27 | "postbuild:production": "npm run _check:links", 28 | "serve": "npm run _serve", 29 | "test": "npm run check:links", 30 | "update:pkg:dep": "npm install --save-dev autoprefixer@latest postcss-cli@latest", 31 | "update:pkg:hugo": "npm install --save-dev --save-exact hugo-extended@latest" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^10.4.14", 35 | "hugo-extended": "0.153.2", 36 | "postcss-cli": "^11.0.0" 37 | } 38 | } -------------------------------------------------------------------------------- /internal/scanners/aa/aa_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package aa 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestAutomationAccountScanner_Init(t *testing.T) { 13 | scanner := NewAutomationAccountScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "test-subscription", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestAutomationAccountScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewAutomationAccountScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | expectedTypes := []string{"Microsoft.Automation/automationAccounts"} 33 | if len(resourceTypes) != len(expectedTypes) { 34 | t.Errorf("ResourceTypes() returned %d types, want %d", len(resourceTypes), len(expectedTypes)) 35 | } 36 | 37 | if resourceTypes[0] != expectedTypes[0] { 38 | t.Errorf("ResourceTypes() = %v, want %v", resourceTypes, expectedTypes) 39 | } 40 | } 41 | 42 | func TestAutomationAccountScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewAutomationAccountScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | // Current implementation returns empty map 47 | if recommendations == nil { 48 | t.Error("GetRecommendations() returned nil") 49 | } 50 | 51 | if len(recommendations) != 0 { 52 | t.Errorf("GetRecommendations() returned %d recommendations, expected 0", len(recommendations)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/to/string_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package to 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestString(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | input interface{} 14 | want string 15 | }{ 16 | { 17 | name: "nil value", 18 | input: nil, 19 | want: "", 20 | }, 21 | { 22 | name: "string value", 23 | input: "hello", 24 | want: "hello", 25 | }, 26 | { 27 | name: "empty string", 28 | input: "", 29 | want: "", 30 | }, 31 | { 32 | name: "int value", 33 | input: 42, 34 | want: "42", 35 | }, 36 | { 37 | name: "negative int", 38 | input: -100, 39 | want: "-100", 40 | }, 41 | { 42 | name: "zero int", 43 | input: 0, 44 | want: "0", 45 | }, 46 | { 47 | name: "bool true", 48 | input: true, 49 | want: "true", 50 | }, 51 | { 52 | name: "bool false", 53 | input: false, 54 | want: "false", 55 | }, 56 | { 57 | name: "struct value", 58 | input: struct{ Name string }{Name: "test"}, 59 | want: `{"Name":"test"}`, 60 | }, 61 | { 62 | name: "map value", 63 | input: map[string]string{"key": "value"}, 64 | want: `{"key":"value"}`, 65 | }, 66 | { 67 | name: "slice value", 68 | input: []string{"a", "b", "c"}, 69 | want: `["a","b","c"]`, 70 | }, 71 | { 72 | name: "float value", 73 | input: 3.14, 74 | want: "3.14", 75 | }, 76 | } 77 | 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | got := String(tt.input) 81 | if got != tt.want { 82 | t.Errorf("String() = %v, want %v", got, tt.want) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /docs/content/en/docs/Contributing/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contribution Guidelines 3 | weight: 5 4 | description: How to contribute to the project 5 | --- 6 | 7 | ## Contributing 8 | 9 | This project welcomes contributions and suggestions. Most contributions require you to 10 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 11 | and actually do, grant us the rights to use your contribution. For details, visit 12 | https://cla.microsoft.com. 13 | 14 | When you submit a pull request, a CLA-bot will automatically determine whether you need 15 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 16 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 17 | 18 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 19 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 20 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 21 | 22 | ## Contributing to Documentation 23 | 24 | Below are the steps and required packages to get the Azure Quick Review Hugo site to build and run locally. 25 | 26 | * Ensure that you have the following packages installed locally. 27 | 28 | - git 29 | - hugo extended 30 | - nodejs 31 | 32 | * Fork the azqr repository, clone locally and then head to the docs folder 33 | 34 | ``` powershell 35 | cd .\azqr\docs 36 | ``` 37 | 38 | * Execute the Node Module installer 39 | 40 | ``` console 41 | npm install 42 | ``` 43 | 44 | * Once this has finish you can execute the Hugo Server 45 | 46 | ``` console 47 | hugo server 48 | ``` -------------------------------------------------------------------------------- /internal/scanners/it/rules.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package it 5 | 6 | import ( 7 | "strings" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/virtualmachineimagebuilder/armvirtualmachineimagebuilder/v2" 11 | ) 12 | 13 | // GetRules - Returns the rules for the ImageTemplateScanner 14 | func (a *ImageTemplateScanner) GetRecommendations() map[string]models.AzqrRecommendation { 15 | return map[string]models.AzqrRecommendation{ 16 | "it-006": { 17 | RecommendationID: "it-006", 18 | ResourceType: "Microsoft.VirtualMachineImages/imageTemplates", 19 | Category: models.CategoryGovernance, 20 | Recommendation: "Image Template Name should comply with naming conventions", 21 | Impact: models.ImpactLow, 22 | Eval: func(target interface{}, scanContext *models.ScanContext) (bool, string) { 23 | c := target.(*armvirtualmachineimagebuilder.ImageTemplate) 24 | caf := strings.HasPrefix(*c.Name, "it") 25 | return !caf, "" 26 | }, 27 | LearnMoreUrl: "https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations", 28 | }, 29 | "it-007": { 30 | RecommendationID: "it-007", 31 | ResourceType: "Microsoft.VirtualMachineImages/imageTemplates", 32 | Category: models.CategoryGovernance, 33 | Recommendation: "Image Template should have tags", 34 | Impact: models.ImpactLow, 35 | Eval: func(target interface{}, scanContext *models.ScanContext) (bool, string) { 36 | c := target.(*armvirtualmachineimagebuilder.ImageTemplate) 37 | return len(c.Tags) == 0, "" 38 | }, 39 | LearnMoreUrl: "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources?tabs=json", 40 | }, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/to/ptr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package to 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestPtr(t *testing.T) { 11 | t.Run("string pointer", func(t *testing.T) { 12 | val := "test" 13 | ptr := Ptr(val) 14 | if ptr == nil { 15 | t.Fatal("Ptr() returned nil") 16 | } 17 | if *ptr != val { 18 | t.Errorf("Ptr() = %v, want %v", *ptr, val) 19 | } 20 | }) 21 | 22 | t.Run("int pointer", func(t *testing.T) { 23 | val := 42 24 | ptr := Ptr(val) 25 | if ptr == nil { 26 | t.Fatal("Ptr() returned nil") 27 | } 28 | if *ptr != val { 29 | t.Errorf("Ptr() = %v, want %v", *ptr, val) 30 | } 31 | }) 32 | 33 | t.Run("bool pointer", func(t *testing.T) { 34 | val := true 35 | ptr := Ptr(val) 36 | if ptr == nil { 37 | t.Fatal("Ptr() returned nil") 38 | } 39 | if *ptr != val { 40 | t.Errorf("Ptr() = %v, want %v", *ptr, val) 41 | } 42 | }) 43 | 44 | t.Run("struct pointer", func(t *testing.T) { 45 | type TestStruct struct { 46 | Name string 47 | Age int 48 | } 49 | val := TestStruct{Name: "test", Age: 30} 50 | ptr := Ptr(val) 51 | if ptr == nil { 52 | t.Fatal("Ptr() returned nil") 53 | } 54 | if ptr.Name != val.Name || ptr.Age != val.Age { 55 | t.Errorf("Ptr() = %v, want %v", *ptr, val) 56 | } 57 | }) 58 | 59 | t.Run("empty string pointer", func(t *testing.T) { 60 | val := "" 61 | ptr := Ptr(val) 62 | if ptr == nil { 63 | t.Fatal("Ptr() returned nil") 64 | } 65 | if *ptr != val { 66 | t.Errorf("Ptr() = %v, want %v", *ptr, val) 67 | } 68 | }) 69 | 70 | t.Run("zero int pointer", func(t *testing.T) { 71 | val := 0 72 | ptr := Ptr(val) 73 | if ptr == nil { 74 | t.Fatal("Ptr() returned nil") 75 | } 76 | if *ptr != val { 77 | t.Errorf("Ptr() = %v, want %v", *ptr, val) 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /scripts/verify-checksum.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to verify azqr binary checksum 4 | # Usage: ./scripts/verify-checksum.sh 5 | # Example: ./scripts/verify-checksum.sh 2.7.3 win-amd64 6 | 7 | set -e 8 | 9 | VERSION=$1 10 | PLATFORM=$2 11 | 12 | if [ -z "$VERSION" ] || [ -z "$PLATFORM" ]; then 13 | echo "Usage: $0 " 14 | echo "Examples:" 15 | echo " $0 2.7.3 win-amd64" 16 | echo " $0 2.7.3 linux-amd64" 17 | echo " $0 2.7.3 darwin-amd64" 18 | exit 1 19 | fi 20 | 21 | # Define URLs 22 | BASE_URL="https://github.com/Azure/azqr/releases/download/v.${VERSION}" 23 | FILE_NAME="azqr-${PLATFORM}.zip" 24 | CHECKSUM_NAME="${FILE_NAME}.sha256" 25 | 26 | echo "Verifying azqr ${VERSION} for ${PLATFORM}..." 27 | echo "File: ${FILE_NAME}" 28 | echo "Checksum: ${CHECKSUM_NAME}" 29 | 30 | # Check if files exist locally 31 | if [ ! -f "$FILE_NAME" ]; then 32 | echo "Error: ${FILE_NAME} not found in current directory" 33 | echo "Please download it first:" 34 | echo " curl -L ${BASE_URL}/${FILE_NAME} -o ${FILE_NAME}" 35 | exit 1 36 | fi 37 | 38 | # Download checksum if not exists 39 | if [ ! -f "$CHECKSUM_NAME" ]; then 40 | echo "Downloading checksum file..." 41 | echo " curl -sL ${BASE_URL}/${CHECKSUM_NAME} -o $CHECKSUM_NAME" 42 | curl -sL "${BASE_URL}/${CHECKSUM_NAME}" -o "$CHECKSUM_NAME" 43 | fi 44 | 45 | # Verify checksum 46 | echo "Verifying checksum..." 47 | if command -v sha256sum >/dev/null 2>&1; then 48 | # Linux/Unix 49 | sha256sum -c "$CHECKSUM_NAME" 50 | elif command -v shasum >/dev/null 2>&1; then 51 | # macOS 52 | shasum -a 256 -c "$CHECKSUM_NAME" 53 | else 54 | echo "Error: No suitable checksum command found (sha256sum or shasum)" 55 | exit 1 56 | fi 57 | 58 | echo "✅ Checksum verification successful!" 59 | echo "The file ${FILE_NAME} is authentic and has not been tampered with." -------------------------------------------------------------------------------- /docs/content/en/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Azure Quick Review 3 | --- 4 | 5 | {{< blocks/cover title="Azure Quick Review!" image_anchor="top" height="full" >}} 6 | 7 | Learn More 8 | 9 | 10 | GitHub Repo 11 | 12 |

Analyze Azure resources and identify whether they comply with Azure's best practices and recommendations.

13 | {{< blocks/link-down color="info" >}} 14 | {{< /blocks/cover >}} 15 | 16 | 17 | {{% blocks/lead color="primary" %}} 18 | 19 | **Azure Quick Review (azqr)** is a command-line interface (CLI) tool specifically designed to analyze Azure resources and identify whether they comply with Azure's best practices and recommendations. Its primary purpose is to provide users with a detailed overview of their Azure resources, enabling them to easily identify any non-compliant configurations or potential areas for improvement. 20 | 21 | {{% /blocks/lead %}} 22 | 23 | {{% blocks/section color="dark" type="row" %}} 24 | 25 | {{% blocks/feature icon="fa-solid fa-file-lines" title="Read the Docs!" url="https://azure.github.io/azqr/docs" %}} 26 | Learn how to use Azure Quick Review (azqr) 27 | {{% /blocks/feature %}} 28 | 29 | {{% blocks/feature icon="fa-solid fa-laptop-code" title="Install azqr!" url="https://github.com/azure/azqr/" %}} 30 | Learn how to install Azure Quick Review (azqr) 31 | {{% /blocks/feature %}} 32 | 33 | {{% blocks/feature icon="fa-solid fa-code-pull-request" title="Contributions welcome!" url="https://github.com/azure/azqr" %}} 34 | We do a [Pull Request](https://github.com/azure/azqr/pulls) contributions workflow on **GitHub**. New users are always welcome! 35 | {{% /blocks/feature %}} 36 | 37 | {{% /blocks/section %}} 38 | -------------------------------------------------------------------------------- /internal/scanners/sap/sap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package sap 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestSAPScanner_Init(t *testing.T) { 13 | scanner := NewSAPScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestSAPScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewSAPScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestSAPScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewSAPScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | func TestSAPScanner_Scan(t *testing.T) { 52 | scanner := NewSAPScanner() 53 | config := &models.ScannerConfig{ 54 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 55 | SubscriptionName: "Test Subscription", 56 | } 57 | if err := scanner.Init(config); err != nil { 58 | t.Fatalf("Init() returned unexpected error: %v", err) 59 | } 60 | 61 | scanContext := &models.ScanContext{} 62 | 63 | results, err := scanner.Scan(scanContext) 64 | if err != nil { 65 | t.Errorf("Scan() returned unexpected error: %v", err) 66 | } 67 | 68 | if results == nil { 69 | t.Fatal("Scan() returned nil results") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/cicd/azdo-pipeline.yml: -------------------------------------------------------------------------------- 1 | # AZDO pipeline to run azqr scan and publish the action plan 2 | 3 | # Trigger the pipeline manually or every Friday night 4 | schedules: 5 | - cron: "0 0 * * 5" # Every Friday at midnight 6 | displayName: "Weekly Friday Night Trigger" 7 | branches: 8 | include: 9 | - main 10 | 11 | # Trigger the pipeline on every push to main branch 12 | trigger: 13 | branches: 14 | include: 15 | - main 16 | 17 | # Trigger the pipeline on every pull request to main branch 18 | pr: 19 | branches: 20 | include: 21 | - main 22 | 23 | pool: 24 | vmImage: ubuntu-latest 25 | 26 | steps: 27 | - script: | 28 | latest_azqr=$(curl -sL https://api.github.com/repos/Azure/azqr/releases/latest | jq -r ".tag_name" | cut -c1-) \ 29 | && wget https://github.com/Azure/azqr/releases/download/$latest_azqr/azqr-linux-amd64.zip -O azqr.zip \ 30 | && unzip -uj -qq azqr.zip -d /usr/local/bin \ 31 | && rm azqr.zip \ 32 | && chmod +x /usr/local/bin/azqr 33 | displayName: "Install azqr" 34 | 35 | - task: AzureCLI@2 36 | inputs: 37 | azureSubscription: "" 38 | addSpnToEnvironment: true 39 | scriptType: "bash" 40 | scriptLocation: "inlineScript" 41 | inlineScript: | 42 | export AZURE_CLIENT_ID=$servicePrincipalId 43 | export AZURE_CLIENT_SECRET=$servicePrincipalKey 44 | export AZURE_TENANT_ID=$tenantId 45 | timestamp=$( date '+%Y%m%d%H%M%S' ) 46 | echo "##vso[task.setvariable variable=DATETIME]$timestamp" 47 | azqr scan -o "$(System.DefaultWorkingDirectory)/azqr_action_plan_$timestamp" 48 | displayName: "Run azqr scan" 49 | 50 | - task: PublishPipelineArtifact@1 51 | inputs: 52 | targetPath: "$(System.DefaultWorkingDirectory)/azqr_action_plan_$(DATETIME).xlsx" 53 | artifact: "azqr_result" 54 | publishLocation: "pipeline" 55 | displayName: "Publish azqr action plan" 56 | -------------------------------------------------------------------------------- /internal/graph/azure-orphan-resources/Web/queries.yaml: -------------------------------------------------------------------------------- 1 | - description: App Service plans without hosting Apps 2 | aprlGuid: 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d 3 | recommendationTypeId: null 4 | recommendationControl: Governance 5 | recommendationImpact: Medium 6 | recommendationResourceType: Microsoft.Web/serverFarms 7 | recommendationMetadataState: Active 8 | longDescription: | 9 | App Service plans without hosting Apps. 10 | potentialBenefits: Identifies unused resources 11 | pgVerified: false 12 | automationAvailable: false 13 | tags: [] 14 | learnMoreLink: 15 | - name: App Service plans 16 | url: "https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans" 17 | 18 | - description: API Connections not related to any Logic App 19 | aprlGuid: 2d3e4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a 20 | recommendationTypeId: null 21 | recommendationControl: Governance 22 | recommendationImpact: Medium 23 | recommendationResourceType: Microsoft.Web/connections 24 | recommendationMetadataState: Active 25 | longDescription: | 26 | API Connections not related to any Logic App. 27 | potentialBenefits: Identifies unused resources 28 | pgVerified: false 29 | automationAvailable: false 30 | tags: [] 31 | learnMoreLink: 32 | - name: API Connections 33 | url: "https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-overview" 34 | 35 | - description: Expired certificates 36 | aprlGuid: 3e4f5a6b-7c8d-9e0f-1a2b-3c4d5e6f7a8b 37 | recommendationTypeId: null 38 | recommendationControl: Governance 39 | recommendationImpact: Medium 40 | recommendationResourceType: Microsoft.Web/certificates 41 | recommendationMetadataState: Active 42 | longDescription: | 43 | Expired certificates. 44 | potentialBenefits: Identifies expired certificates 45 | pgVerified: false 46 | automationAvailable: false 47 | tags: [] 48 | learnMoreLink: 49 | - name: Certificates 50 | url: "https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-certificate" 51 | -------------------------------------------------------------------------------- /internal/scanners/vdpool/vdpool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package vdpool 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestVirtualDesktopScanner_Init(t *testing.T) { 13 | scanner := NewVirtualDesktopScanner() 14 | config := &models.ScannerConfig{ 15 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 16 | } 17 | 18 | err := scanner.Init(config) 19 | if err != nil { 20 | t.Errorf("Init() returned unexpected error: %v", err) 21 | } 22 | 23 | if scanner.GetConfig() != config { 24 | t.Error("Init() did not set config properly") 25 | } 26 | } 27 | 28 | func TestVirtualDesktopScanner_ResourceTypes(t *testing.T) { 29 | scanner := NewVirtualDesktopScanner() 30 | resourceTypes := scanner.ResourceTypes() 31 | 32 | if len(resourceTypes) == 0 { 33 | t.Error("ResourceTypes() returned empty slice") 34 | } 35 | 36 | // Just verify we get at least one resource type 37 | if resourceTypes[0] == "" { 38 | t.Error("ResourceTypes() returned empty string") 39 | } 40 | } 41 | 42 | func TestVirtualDesktopScanner_GetRecommendations(t *testing.T) { 43 | scanner := NewVirtualDesktopScanner() 44 | recommendations := scanner.GetRecommendations() 45 | 46 | if recommendations == nil { 47 | t.Error("GetRecommendations() returned nil") 48 | } 49 | } 50 | 51 | func TestVirtualDesktopScanner_Scan(t *testing.T) { 52 | scanner := NewVirtualDesktopScanner() 53 | config := &models.ScannerConfig{ 54 | SubscriptionID: "00000000-0000-0000-0000-000000000000", 55 | SubscriptionName: "Test Subscription", 56 | } 57 | if err := scanner.Init(config); err != nil { 58 | t.Fatalf("Init() returned unexpected error: %v", err) 59 | } 60 | 61 | scanContext := &models.ScanContext{} 62 | 63 | results, err := scanner.Scan(scanContext) 64 | if err != nil { 65 | t.Errorf("Scan() returned unexpected error: %v", err) 66 | } 67 | 68 | if results == nil { 69 | t.Fatal("Scan() returned nil results") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/viewer/server_test.go: -------------------------------------------------------------------------------- 1 | package viewer 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | var sample = &DataStore{Data: map[string][]map[string]string{ 10 | DataSetRecommendations: {{"implemented": "true", "recommendationId": "r1"}, {"implemented": "false", "recommendationId": "r2"}}, 11 | DataSetImpacted: {{"recommendationId": "r2", "resourceId": "res1"}}, 12 | DataSetResourceType: {{"resourceType": "Microsoft.Compute/virtualMachines"}}, 13 | DataSetInventory: {{"resourceType": "Microsoft.Compute/virtualMachines", "resourceName": "vm1"}}, 14 | DataSetAdvisor: {}, 15 | DataSetAzurePolicy: {{"complianceState": "NonCompliant"}}, 16 | DataSetDefender: {}, 17 | DataSetDefenderRecommendations: {}, 18 | DataSetCosts: {{"value": "10.50"}}, 19 | DataSetOutOfScope: {}, 20 | }} 21 | 22 | func TestSummaryEndpoint(t *testing.T) { 23 | srv := httptest.NewServer(NewHandler(sample)) 24 | defer srv.Close() 25 | resp, err := http.Get(srv.URL + "/api/summary") 26 | if err != nil { 27 | t.Fatalf("request failed: %v", err) 28 | } 29 | if resp.StatusCode != http.StatusOK { 30 | t.Fatalf("unexpected status: %d", resp.StatusCode) 31 | } 32 | } 33 | 34 | func TestFilterEndpoint(t *testing.T) { 35 | srv := httptest.NewServer(NewHandler(sample)) 36 | defer srv.Close() 37 | resp, err := http.Get(srv.URL + "/api/data/recommendations?implemented=true") 38 | if err != nil { 39 | t.Fatalf("request failed: %v", err) 40 | } 41 | if resp.StatusCode != http.StatusOK { 42 | t.Fatalf("unexpected status: %d", resp.StatusCode) 43 | } 44 | } 45 | 46 | func TestAnalyticsEndpoint(t *testing.T) { 47 | srv := httptest.NewServer(NewHandler(sample)) 48 | defer srv.Close() 49 | resp, err := http.Get(srv.URL + "/api/analytics") 50 | if err != nil { 51 | t.Fatalf("request failed: %v", err) 52 | } 53 | if resp.StatusCode != http.StatusOK { 54 | t.Fatalf("unexpected status: %d", resp.StatusCode) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/plugins/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package plugins 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | "github.com/Azure/azure-sdk-for-go/sdk/azcore" 11 | ) 12 | 13 | // InternalPluginScanner defines the interface for internal plugins 14 | type InternalPluginScanner interface { 15 | // Scan executes the plugin and returns table data 16 | Scan(ctx context.Context, cred azcore.TokenCredential, subscriptions map[string]string, filters *models.Filters) (*ExternalPluginOutput, error) 17 | 18 | // GetMetadata returns metadata about the plugin 19 | GetMetadata() PluginMetadata 20 | } 21 | 22 | // internalPluginRegistry holds all registered internal plugins 23 | var internalPluginRegistry = make(map[string]InternalPluginScanner) 24 | 25 | // RegisterInternalPlugin registers an internal plugin 26 | func RegisterInternalPlugin(name string, scanner InternalPluginScanner) { 27 | internalPluginRegistry[name] = scanner 28 | } 29 | 30 | // GetInternalPlugin retrieves a registered internal plugin 31 | func GetInternalPlugin(name string) (InternalPluginScanner, bool) { 32 | scanner, exists := internalPluginRegistry[name] 33 | return scanner, exists 34 | } 35 | 36 | // ListInternalPlugins returns all registered internal plugins 37 | func ListInternalPlugins() []string { 38 | names := make([]string, 0, len(internalPluginRegistry)) 39 | for name := range internalPluginRegistry { 40 | names = append(names, name) 41 | } 42 | return names 43 | } 44 | 45 | // discoverInternalPlugins converts internal plugins to Plugin objects 46 | func discoverInternalPlugins() []*Plugin { 47 | plugins := make([]*Plugin, 0, len(internalPluginRegistry)) 48 | 49 | for _, scanner := range internalPluginRegistry { 50 | metadata := scanner.GetMetadata() 51 | metadata.Type = PluginTypeInternal 52 | 53 | plugin := &Plugin{ 54 | Metadata: metadata, 55 | InternalScanner: scanner, 56 | } 57 | 58 | plugins = append(plugins, plugin) 59 | } 60 | 61 | return plugins 62 | } 63 | -------------------------------------------------------------------------------- /internal/scanners/nsg/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package nsg 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestNSGScanner_ResourceTypes(t *testing.T) { 13 | scanner := &NSGScanner{} 14 | resourceTypes := scanner.ResourceTypes() 15 | 16 | if len(resourceTypes) == 0 { 17 | t.Error("Expected at least one resource type, got none") 18 | } 19 | 20 | expectedType := "Microsoft.Network/networkSecurityGroups" 21 | found := false 22 | for _, rt := range resourceTypes { 23 | if rt == expectedType { 24 | found = true 25 | break 26 | } 27 | } 28 | 29 | if !found { 30 | t.Errorf("Expected resource type %s not found in %v", expectedType, resourceTypes) 31 | } 32 | } 33 | 34 | func TestNSGScanner_GetRecommendations(t *testing.T) { 35 | scanner := &NSGScanner{} 36 | recommendations := scanner.GetRecommendations() 37 | 38 | if len(recommendations) == 0 { 39 | t.Error("Expected recommendations, got none") 40 | } 41 | 42 | for id, rec := range recommendations { 43 | if rec.RecommendationID != id { 44 | t.Errorf("Recommendation ID mismatch: key=%s, ID=%s", id, rec.RecommendationID) 45 | } 46 | if rec.Recommendation == "" { 47 | t.Errorf("Recommendation %s has empty Recommendation text", id) 48 | } 49 | if rec.Category == "" { 50 | t.Errorf("Recommendation %s has empty Category", id) 51 | } 52 | if rec.Eval == nil { 53 | t.Errorf("Recommendation %s has nil Eval function", id) 54 | } 55 | } 56 | } 57 | 58 | func TestNSGScanner_Init(t *testing.T) { 59 | scanner := &NSGScanner{} 60 | 61 | config := &models.ScannerConfig{ 62 | SubscriptionID: "test-subscription", 63 | Cred: nil, 64 | ClientOptions: nil, 65 | } 66 | 67 | err := scanner.Init(config) 68 | if err != nil { 69 | t.Errorf("Init failed: %v", err) 70 | } 71 | // Config verification removed - scanner doesn't expose GetConfig() 72 | } 73 | 74 | func TestNSGScanner_Scan(t *testing.T) { 75 | scanner := &NSGScanner{} 76 | var _ = scanner.Scan 77 | t.Log("Scan method signature verified") 78 | } 79 | -------------------------------------------------------------------------------- /internal/scanners/appi/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package appi 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/Azure/azqr/internal/models" 11 | "github.com/Azure/azqr/internal/to" 12 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights" 13 | ) 14 | 15 | func TestAppInsightsScanner_Rules(t *testing.T) { 16 | type fields struct { 17 | rule string 18 | target interface{} 19 | scanContext *models.ScanContext 20 | } 21 | type want struct { 22 | broken bool 23 | result string 24 | } 25 | tests := []struct { 26 | name string 27 | fields fields 28 | want want 29 | }{ 30 | { 31 | name: "AppInsightsScanner SLA", 32 | fields: fields{ 33 | rule: "appi-001", 34 | target: &armapplicationinsights.Component{}, 35 | scanContext: &models.ScanContext{}, 36 | }, 37 | want: want{ 38 | broken: false, 39 | result: "99.9%", 40 | }, 41 | }, 42 | { 43 | name: "AppInsightsScanner CAF", 44 | fields: fields{ 45 | rule: "appi-002", 46 | target: &armapplicationinsights.Component{ 47 | Name: to.Ptr("appi-test"), 48 | }, 49 | scanContext: &models.ScanContext{}, 50 | }, 51 | want: want{ 52 | broken: false, 53 | result: "", 54 | }, 55 | }, 56 | { 57 | name: "AppInsightsScanner tags", 58 | fields: fields{ 59 | rule: "appi-003", 60 | target: &armapplicationinsights.Component{}, 61 | scanContext: &models.ScanContext{}, 62 | }, 63 | want: want{ 64 | broken: true, 65 | result: "", 66 | }, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | s := &AppInsightsScanner{} 72 | rules := s.GetRecommendations() 73 | b, w := rules[tt.fields.rule].Eval(tt.fields.target, tt.fields.scanContext) 74 | got := want{ 75 | broken: b, 76 | result: w, 77 | } 78 | if !reflect.DeepEqual(got, tt.want) { 79 | t.Errorf("AppInsightsScanner Rule.Eval() = %v, want %v", got, tt.want) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/scanners/pip/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package pip 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestPublicIPScanner_ResourceTypes(t *testing.T) { 13 | scanner := &PublicIPScanner{} 14 | resourceTypes := scanner.ResourceTypes() 15 | 16 | if len(resourceTypes) == 0 { 17 | t.Error("Expected at least one resource type, got none") 18 | } 19 | 20 | expectedType := "Microsoft.Network/publicIPAddresses" 21 | found := false 22 | for _, rt := range resourceTypes { 23 | if rt == expectedType { 24 | found = true 25 | break 26 | } 27 | } 28 | 29 | if !found { 30 | t.Errorf("Expected resource type %s not found in %v", expectedType, resourceTypes) 31 | } 32 | } 33 | 34 | func TestPublicIPScanner_GetRecommendations(t *testing.T) { 35 | scanner := &PublicIPScanner{} 36 | recommendations := scanner.GetRecommendations() 37 | 38 | if len(recommendations) == 0 { 39 | t.Error("Expected recommendations, got none") 40 | } 41 | 42 | for id, rec := range recommendations { 43 | if rec.RecommendationID != id { 44 | t.Errorf("Recommendation ID mismatch: key=%s, ID=%s", id, rec.RecommendationID) 45 | } 46 | if rec.Recommendation == "" { 47 | t.Errorf("Recommendation %s has empty Recommendation text", id) 48 | } 49 | if rec.Category == "" { 50 | t.Errorf("Recommendation %s has empty Category", id) 51 | } 52 | if rec.Eval == nil { 53 | t.Errorf("Recommendation %s has nil Eval function", id) 54 | } 55 | } 56 | } 57 | 58 | func TestPublicIPScanner_Init(t *testing.T) { 59 | scanner := &PublicIPScanner{} 60 | 61 | config := &models.ScannerConfig{ 62 | SubscriptionID: "test-subscription", 63 | Cred: nil, 64 | ClientOptions: nil, 65 | } 66 | 67 | err := scanner.Init(config) 68 | if err != nil { 69 | t.Errorf("Init failed: %v", err) 70 | } 71 | // Config verification removed - scanner doesn't expose GetConfig() 72 | } 73 | 74 | func TestPublicIPScanner_Scan(t *testing.T) { 75 | scanner := &PublicIPScanner{} 76 | var _ = scanner.Scan 77 | t.Log("Scan method signature verified") 78 | } 79 | -------------------------------------------------------------------------------- /internal/scanners/vm/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package vm 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/Azure/azqr/internal/models" 11 | "github.com/Azure/azqr/internal/to" 12 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4" 13 | ) 14 | 15 | func TestVirtualMachineScanner_Rules(t *testing.T) { 16 | type fields struct { 17 | rule string 18 | target interface{} 19 | scanContext *models.ScanContext 20 | } 21 | type want struct { 22 | broken bool 23 | result string 24 | } 25 | tests := []struct { 26 | name string 27 | fields fields 28 | want want 29 | }{ 30 | { 31 | name: "VirtualMachineScanner SLA 99.9%", 32 | fields: fields{ 33 | rule: "vm-003", 34 | target: &armcompute.VirtualMachine{ 35 | Properties: &armcompute.VirtualMachineProperties{}, 36 | }, 37 | scanContext: &models.ScanContext{}, 38 | }, 39 | want: want{ 40 | broken: false, 41 | result: "99.9%", 42 | }, 43 | }, 44 | { 45 | name: "VirtualMachineScanner CAF", 46 | fields: fields{ 47 | rule: "vm-006", 48 | target: &armcompute.VirtualMachine{ 49 | Name: to.Ptr("vm-test"), 50 | }, 51 | scanContext: &models.ScanContext{}, 52 | }, 53 | want: want{ 54 | broken: false, 55 | result: "", 56 | }, 57 | }, 58 | { 59 | name: "VirtualMachineScanner Tags", 60 | fields: fields{ 61 | rule: "vm-007", 62 | target: &armcompute.VirtualMachine{}, 63 | scanContext: &models.ScanContext{}, 64 | }, 65 | want: want{ 66 | broken: true, 67 | result: "", 68 | }, 69 | }, 70 | } 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | s := &VirtualMachineScanner{} 74 | rules := s.GetRecommendations() 75 | b, w := rules[tt.fields.rule].Eval(tt.fields.target, tt.fields.scanContext) 76 | got := want{ 77 | broken: b, 78 | result: w, 79 | } 80 | if !reflect.DeepEqual(got, tt.want) { 81 | t.Errorf("VirtualMachineScanner Rule.Eval() = %v, want %v", got, tt.want) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /internal/scanners/ng/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package ng 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestNatGatewayScanner_ResourceTypes(t *testing.T) { 13 | scanner := &NatGatewayScanner{} 14 | resourceTypes := scanner.ResourceTypes() 15 | 16 | if len(resourceTypes) == 0 { 17 | t.Error("Expected at least one resource type, got none") 18 | } 19 | 20 | expectedType := "Microsoft.Network/natGateways" 21 | found := false 22 | for _, rt := range resourceTypes { 23 | if rt == expectedType { 24 | found = true 25 | break 26 | } 27 | } 28 | 29 | if !found { 30 | t.Errorf("Expected resource type %s not found in %v", expectedType, resourceTypes) 31 | } 32 | } 33 | 34 | func TestNatGatewayScanner_GetRecommendations(t *testing.T) { 35 | scanner := &NatGatewayScanner{} 36 | recommendations := scanner.GetRecommendations() 37 | 38 | if len(recommendations) == 0 { 39 | t.Error("Expected recommendations, got none") 40 | } 41 | 42 | for id, rec := range recommendations { 43 | if rec.RecommendationID != id { 44 | t.Errorf("Recommendation ID mismatch: key=%s, ID=%s", id, rec.RecommendationID) 45 | } 46 | if rec.Recommendation == "" { 47 | t.Errorf("Recommendation %s has empty Recommendation text", id) 48 | } 49 | if rec.Category == "" { 50 | t.Errorf("Recommendation %s has empty Category", id) 51 | } 52 | if rec.Eval == nil { 53 | t.Errorf("Recommendation %s has nil Eval function", id) 54 | } 55 | } 56 | } 57 | 58 | func TestNatGatewayScanner_Init(t *testing.T) { 59 | scanner := &NatGatewayScanner{} 60 | 61 | config := &models.ScannerConfig{ 62 | SubscriptionID: "test-subscription", 63 | Cred: nil, 64 | ClientOptions: nil, 65 | } 66 | 67 | err := scanner.Init(config) 68 | if err != nil { 69 | t.Errorf("Init failed: %v", err) 70 | } 71 | // Config verification removed - scanner doesn't expose GetConfig() 72 | } 73 | 74 | func TestNatGatewayScanner_Scan(t *testing.T) { 75 | scanner := &NatGatewayScanner{} 76 | var _ = scanner.Scan 77 | t.Log("Scan method signature verified") 78 | } 79 | -------------------------------------------------------------------------------- /internal/scanners/rt/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package rt 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestRouteTableScanner_ResourceTypes(t *testing.T) { 13 | scanner := &RouteTableScanner{} 14 | resourceTypes := scanner.ResourceTypes() 15 | 16 | if len(resourceTypes) == 0 { 17 | t.Error("Expected at least one resource type, got none") 18 | } 19 | 20 | expectedType := "Microsoft.Network/routeTables" 21 | found := false 22 | for _, rt := range resourceTypes { 23 | if rt == expectedType { 24 | found = true 25 | break 26 | } 27 | } 28 | 29 | if !found { 30 | t.Errorf("Expected resource type %s not found in %v", expectedType, resourceTypes) 31 | } 32 | } 33 | 34 | func TestRouteTableScanner_GetRecommendations(t *testing.T) { 35 | scanner := &RouteTableScanner{} 36 | recommendations := scanner.GetRecommendations() 37 | 38 | if len(recommendations) == 0 { 39 | t.Error("Expected recommendations, got none") 40 | } 41 | 42 | for id, rec := range recommendations { 43 | if rec.RecommendationID != id { 44 | t.Errorf("Recommendation ID mismatch: key=%s, ID=%s", id, rec.RecommendationID) 45 | } 46 | if rec.Recommendation == "" { 47 | t.Errorf("Recommendation %s has empty Recommendation text", id) 48 | } 49 | if rec.Category == "" { 50 | t.Errorf("Recommendation %s has empty Category", id) 51 | } 52 | if rec.Eval == nil { 53 | t.Errorf("Recommendation %s has nil Eval function", id) 54 | } 55 | } 56 | } 57 | 58 | func TestRouteTableScanner_Init(t *testing.T) { 59 | scanner := &RouteTableScanner{} 60 | 61 | config := &models.ScannerConfig{ 62 | SubscriptionID: "test-subscription", 63 | Cred: nil, 64 | ClientOptions: nil, 65 | } 66 | 67 | err := scanner.Init(config) 68 | if err != nil { 69 | t.Errorf("Init failed: %v", err) 70 | } 71 | // Config verification removed - scanner doesn't expose GetConfig() 72 | } 73 | 74 | func TestRouteTableScanner_Scan(t *testing.T) { 75 | scanner := &RouteTableScanner{} 76 | var _ = scanner.Scan 77 | t.Log("Scan method signature verified") 78 | } 79 | -------------------------------------------------------------------------------- /internal/scanners/pip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package scanners 5 | 6 | import ( 7 | "github.com/Azure/azqr/internal/models" 8 | "github.com/Azure/azqr/internal/throttling" 9 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | // PublicIPScanner - Scanner for Public IPs 14 | type PublicIPScanner struct { 15 | config *models.ScannerConfig 16 | client *armnetwork.PublicIPAddressesClient 17 | } 18 | 19 | // Init - Initializes the PublicIPScanner 20 | func (s *PublicIPScanner) Init(config *models.ScannerConfig) error { 21 | s.config = config 22 | var err error 23 | s.client, err = armnetwork.NewPublicIPAddressesClient(s.config.SubscriptionID, s.config.Cred, config.ClientOptions) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | // ListPublicIPs - Lists all Public IPs 31 | func (s *PublicIPScanner) ListPublicIPs() (map[string]*armnetwork.PublicIPAddress, error) { 32 | models.LogSubscriptionScan(s.config.SubscriptionID, "Public IPs") 33 | 34 | res := map[string]*armnetwork.PublicIPAddress{} 35 | opt := armnetwork.PublicIPAddressesClientListAllOptions{} 36 | 37 | pager := s.client.NewListAllPager(&opt) 38 | 39 | for pager.More() { 40 | // Wait for a token from the burstLimiter channel before making the request 41 | _ = throttling.WaitARM(s.config.Ctx); // nolint:errcheck 42 | resp, err := pager.NextPage(s.config.Ctx) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | for _, v := range resp.Value { 48 | res[*v.ID] = v 49 | } 50 | } 51 | 52 | return res, nil 53 | } 54 | 55 | func (s *PublicIPScanner) Scan(config *models.ScannerConfig) map[string]*armnetwork.PublicIPAddress { 56 | err := s.Init(config) 57 | if err != nil { 58 | log.Fatal().Err(err).Msg("Failed to initialize Diagnostic Settings Scanner") 59 | } 60 | pips, err := s.ListPublicIPs() 61 | if err != nil { 62 | if models.ShouldSkipError(err) { 63 | pips = map[string]*armnetwork.PublicIPAddress{} 64 | } else { 65 | log.Fatal().Err(err).Msg("Failed to list Public IPs") 66 | } 67 | } 68 | return pips 69 | } 70 | -------------------------------------------------------------------------------- /internal/scanners/log/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package log 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestLogAnalyticsScanner_ResourceTypes(t *testing.T) { 13 | scanner := &LogAnalyticsScanner{} 14 | resourceTypes := scanner.ResourceTypes() 15 | 16 | if len(resourceTypes) == 0 { 17 | t.Error("Expected at least one resource type, got none") 18 | } 19 | 20 | expectedType := "Microsoft.OperationalInsights/workspaces" 21 | found := false 22 | for _, rt := range resourceTypes { 23 | if rt == expectedType { 24 | found = true 25 | break 26 | } 27 | } 28 | 29 | if !found { 30 | t.Errorf("Expected resource type %s not found in %v", expectedType, resourceTypes) 31 | } 32 | } 33 | 34 | func TestLogAnalyticsScanner_GetRecommendations(t *testing.T) { 35 | scanner := &LogAnalyticsScanner{} 36 | recommendations := scanner.GetRecommendations() 37 | 38 | if len(recommendations) == 0 { 39 | t.Error("Expected recommendations, got none") 40 | } 41 | 42 | for id, rec := range recommendations { 43 | if rec.RecommendationID != id { 44 | t.Errorf("Recommendation ID mismatch: key=%s, ID=%s", id, rec.RecommendationID) 45 | } 46 | if rec.Recommendation == "" { 47 | t.Errorf("Recommendation %s has empty Recommendation text", id) 48 | } 49 | if rec.Category == "" { 50 | t.Errorf("Recommendation %s has empty Category", id) 51 | } 52 | if rec.Eval == nil { 53 | t.Errorf("Recommendation %s has nil Eval function", id) 54 | } 55 | } 56 | } 57 | 58 | func TestLogAnalyticsScanner_Init(t *testing.T) { 59 | scanner := &LogAnalyticsScanner{} 60 | 61 | config := &models.ScannerConfig{ 62 | SubscriptionID: "test-subscription", 63 | Cred: nil, 64 | ClientOptions: nil, 65 | } 66 | 67 | err := scanner.Init(config) 68 | if err != nil { 69 | t.Errorf("Init failed: %v", err) 70 | } 71 | // Config verification removed - scanner doesn't expose GetConfig() 72 | } 73 | 74 | func TestLogAnalyticsScanner_Scan(t *testing.T) { 75 | scanner := &LogAnalyticsScanner{} 76 | var _ = scanner.Scan 77 | t.Log("Scan method signature verified") 78 | } 79 | -------------------------------------------------------------------------------- /internal/scanners/nw/rules_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package nw 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/Azure/azqr/internal/models" 10 | ) 11 | 12 | func TestNetworkWatcherScanner_ResourceTypes(t *testing.T) { 13 | scanner := &NetworkWatcherScanner{} 14 | resourceTypes := scanner.ResourceTypes() 15 | 16 | if len(resourceTypes) == 0 { 17 | t.Error("Expected at least one resource type, got none") 18 | } 19 | 20 | expectedType := "Microsoft.Network/networkWatchers" 21 | found := false 22 | for _, rt := range resourceTypes { 23 | if rt == expectedType { 24 | found = true 25 | break 26 | } 27 | } 28 | 29 | if !found { 30 | t.Errorf("Expected resource type %s not found in %v", expectedType, resourceTypes) 31 | } 32 | } 33 | 34 | func TestNetworkWatcherScanner_GetRecommendations(t *testing.T) { 35 | scanner := &NetworkWatcherScanner{} 36 | recommendations := scanner.GetRecommendations() 37 | 38 | if len(recommendations) == 0 { 39 | t.Error("Expected recommendations, got none") 40 | } 41 | 42 | for id, rec := range recommendations { 43 | if rec.RecommendationID != id { 44 | t.Errorf("Recommendation ID mismatch: key=%s, ID=%s", id, rec.RecommendationID) 45 | } 46 | if rec.Recommendation == "" { 47 | t.Errorf("Recommendation %s has empty Recommendation text", id) 48 | } 49 | if rec.Category == "" { 50 | t.Errorf("Recommendation %s has empty Category", id) 51 | } 52 | if rec.Eval == nil { 53 | t.Errorf("Recommendation %s has nil Eval function", id) 54 | } 55 | } 56 | } 57 | 58 | func TestNetworkWatcherScanner_Init(t *testing.T) { 59 | scanner := &NetworkWatcherScanner{} 60 | 61 | config := &models.ScannerConfig{ 62 | SubscriptionID: "test-subscription", 63 | Cred: nil, 64 | ClientOptions: nil, 65 | } 66 | 67 | err := scanner.Init(config) 68 | if err != nil { 69 | t.Errorf("Init failed: %v", err) 70 | } 71 | // Config verification removed - scanner doesn't expose GetConfig() 72 | } 73 | 74 | func TestNetworkWatcherScanner_Scan(t *testing.T) { 75 | scanner := &NetworkWatcherScanner{} 76 | var _ = scanner.Scan 77 | t.Log("Scan method signature verified") 78 | } 79 | --------------------------------------------------------------------------------