Skip to content

Commit

Permalink
feat: added defender recommendations #325
Browse files Browse the repository at this point in the history
  • Loading branch information
cmendible committed Jan 23, 2025
1 parent fc4cc32 commit d730d73
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 72 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The output generated by **Azure Quick Review (azqr)** is written by default to a
* **ResourceTypes**: a list of impacted resource types.
* **Inventory**: a list of all resources scanned by the tool. Here you'll find details such as SKU, Tier, Kind or calculated SLA.
* **Advisor**: a list of recommendations provided by Azure Advisor.
* **DefenderRecommendations**: a list of recommendations provided by Microsoft Defender for Cloud.
* **Defender**: a list of Microsoft Defender for Cloud plans and their tiers.
* **Costs**: a list of costs associated with the scanned subscription for the last 3 months.

Expand Down
1 change: 1 addition & 0 deletions docs/content/en/docs/Overview/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The output generated by **Azure Quick Review (azqr)** is written by default to a
* **ResourceTypes**: a list of impacted resource types.
* **Inventory**: a list of all resources scanned by the tool. Here you'll find details such as SKU, Tier, Kind or calculated SLA.
* **Advisor**: a list of recommendations provided by Azure Advisor.
* **DefenderRecommendations**: a list of recommendations provided by Microsoft Defender for Cloud.
* **Defender**: a list of Microsoft Defender for Cloud plans and their tiers.
* **Costs**: a list of costs associated with the scanned subscription for the last 3 months.

Expand Down
40 changes: 9 additions & 31 deletions internal/aprl_scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ package internal
import (
"context"
"embed"
"encoding/json"
"fmt"
"io/fs"
"math"
"strings"
Expand All @@ -16,6 +14,7 @@ import (

"github.com/Azure/azqr/internal/azqr"
"github.com/Azure/azqr/internal/graph"
"github.com/Azure/azqr/internal/to"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -218,17 +217,17 @@ func (sc AprlScanner) graphScan(ctx context.Context, graphClient *graph.GraphQue
LongDescription: rule.LongDescription,
PotentialBenefits: rule.PotentialBenefits,
Impact: azqr.RecommendationImpact(rule.Impact),
Name: convertInterfaceToString(m["name"]),
ResourceID: convertInterfaceToString(m["id"]),
Name: to.String(m["name"]),
ResourceID: to.String(m["id"]),
SubscriptionID: subscription,
SubscriptionName: subscriptionName,
ResourceGroup: azqr.GetResourceGroupFromResourceID(m["id"].(string)),
Tags: convertInterfaceToString(m["tags"]),
Param1: convertInterfaceToString(m["param1"]),
Param2: convertInterfaceToString(m["param2"]),
Param3: convertInterfaceToString(m["param3"]),
Param4: convertInterfaceToString(m["param4"]),
Param5: convertInterfaceToString(m["param5"]),
Tags: to.String(m["tags"]),
Param1: to.String(m["param1"]),
Param2: to.String(m["param2"]),
Param3: to.String(m["param3"]),
Param4: to.String(m["param4"]),
Param5: to.String(m["param5"]),
Learn: rule.LearnMoreLink[0].Url,
AutomationAvailable: rule.AutomationAvailable,
Source: "APRL",
Expand Down Expand Up @@ -263,24 +262,3 @@ func (sc AprlScanner) getGraphRules(service string, filters *azqr.Filters, aprl
}
return r
}

func convertInterfaceToString(i interface{}) string {
if i == nil {
return ""
}

switch v := i.(type) {
case string:
return v
case int:
return fmt.Sprintf("%d", v)
case bool:
return fmt.Sprintf("%t", v)
default:
jsonStr, err := json.Marshal(i)
if err != nil {
log.Fatal().Err(err).Msg("Unsupported type found in ARG query result")
}
return string(jsonStr)
}
}
15 changes: 15 additions & 0 deletions internal/azqr/azqr.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ type (
Source string
}

DefenderRecommendation struct {
SubscriptionId string
SubscriptionName string
ResourceGroupName string
ResourceType string
ResourceName string
Category string
RecommendationSeverity string
RecommendationName string
ActionDescription string
RemediationDescription string
AzPortalLink string
ResourceId string
}

RecommendationEngine struct{}

RecommendationImpact string
Expand Down
3 changes: 3 additions & 0 deletions internal/renderers/csv/csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func CreateCsvReport(data *renderers.ReportData) {
records = data.DefenderTable()
writeData(records, data.OutputFileName, "defender")

records = data.DefenderRecommendationsTable()
writeData(records, data.OutputFileName, "defenderRecommendations")

records = data.AdvisorTable()
writeData(records, data.OutputFileName, "advisor")

Expand Down
2 changes: 1 addition & 1 deletion internal/renderers/excel/advisor.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func renderAdvisor(f *excelize.File, data *renderers.ReportData) {
headers := records[0]
createFirstRow(f, "Advisor", headers)

if len(data.AdvisorData) > 0 {
if len(data.Advisor) > 0 {
records = records[1:]
currentRow := 4
for _, row := range records {
Expand Down
4 changes: 2 additions & 2 deletions internal/renderers/excel/cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func renderCosts(f *excelize.File, data *renderers.ReportData) {
records := data.CostTable()
headers := records[0]
createFirstRow(f, "Costs", headers)
if data.CostData != nil && len(data.CostData.Items) > 0 {

if data.Cost != nil && len(data.Cost.Items) > 0 {
records = records[1:]
currentRow := 4
for _, row := range records {
Expand Down
36 changes: 35 additions & 1 deletion internal/renderers/excel/defender.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func renderDefender(f *excelize.File, data *renderers.ReportData) {
headers := records[0]
createFirstRow(f, "Defender", headers)

if len(data.DefenderData) > 0 {
if len(data.Defender) > 0 {
records = records[1:]
currentRow := 4
for _, row := range records {
Expand All @@ -41,3 +41,37 @@ func renderDefender(f *excelize.File, data *renderers.ReportData) {
log.Info().Msg("Skipping Defender. No data to render")
}
}

// renderDefenderRecommendations renders the Defender recommendations to the Excel sheet.
func renderDefenderRecommendations(f *excelize.File, data *renderers.ReportData) {
sheetName := "DefenderRecommendations"
_, err := f.NewSheet(sheetName)
if err != nil {
log.Fatal().Err(err).Msg("Failed to create DefenderRecommendations sheet")
}

records := data.DefenderRecommendationsTable()
headers := records[0]
createFirstRow(f, sheetName, headers)

if len(data.DefenderRecommendations) > 0 {
records = records[1:]
currentRow := 4
for _, row := range records {
currentRow += 1
cell, err := excelize.CoordinatesToCellName(1, currentRow)
if err != nil {
log.Fatal().Err(err).Msg("Failed to get cell")
}
err = f.SetSheetRow(sheetName, cell, &row)
if err != nil {
log.Fatal().Err(err).Msg("Failed to set row")
}
setHyperLink(f, sheetName, 11, currentRow)
}

configureSheet(f, sheetName, headers, currentRow)
} else {
log.Info().Msg("Skipping DefenderRecommendations. No data to render")
}
}
1 change: 1 addition & 0 deletions internal/renderers/excel/excel.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func CreateExcelReport(data *renderers.ReportData) {
renderResourceTypes(f, data)
renderResources(f, data)
renderAdvisor(f, data)
renderDefenderRecommendations(f, data)
renderDefender(f, data)
renderCosts(f, data)
renderRecommendationsPivotTables(f, lastRow)
Expand Down
4 changes: 2 additions & 2 deletions internal/renderers/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func writeData(data []interface{}, fileName, extension string) {
func getResources(data *renderers.ReportData) []renderers.ResourceResult {
rows := []renderers.ResourceResult{}

for _, r := range data.AprlData {
for _, r := range data.Aprl {
row := renderers.ResourceResult{
ValidationAction: "Azure Resource Graph",
RecommendationId: r.RecommendationID,
Expand Down Expand Up @@ -90,6 +90,6 @@ func getResources(data *renderers.ReportData) []renderers.ResourceResult {
// }
// }
// }

return rows
}
83 changes: 55 additions & 28 deletions internal/renderers/report_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ import (

type (
ReportData struct {
OutputFileName string
Mask bool
AzqrData []azqr.AzqrServiceResult
AprlData []azqr.AprlResult
DefenderData []scanners.DefenderResult
AdvisorData []scanners.AdvisorResult
CostData *scanners.CostResult
Recomendations map[string]map[string]azqr.AprlRecommendation
Resources []*azqr.Resource
ResourceTypeCount []azqr.ResourceTypeCount
OutputFileName string
Mask bool
Azqr []azqr.AzqrServiceResult
Aprl []azqr.AprlResult
Defender []scanners.DefenderResult
DefenderRecommendations []azqr.DefenderRecommendation
Advisor []scanners.AdvisorResult
Cost *scanners.CostResult
Recomendations map[string]map[string]azqr.AprlRecommendation
Resources []*azqr.Resource
ResourceTypeCount []azqr.ResourceTypeCount
}

ResourceResult struct {
Expand Down Expand Up @@ -71,7 +72,7 @@ func (rd *ReportData) ResourcesTable() [][]string {
for _, r := range rd.Resources {
sla := ""

for _, a := range rd.AzqrData {
for _, a := range rd.Azqr {
if strings.EqualFold(strings.ToLower(a.ResourceID()), strings.ToLower(r.ID)) {
for _, rc := range a.Recommendations {
if rc.RecommendationType == azqr.TypeSLA {
Expand Down Expand Up @@ -108,7 +109,7 @@ func (rd *ReportData) ImpactedTable() [][]string {
headers := []string{"Validated Using", "Source", "Category", "Impact", "Resource Type", "Recommendation", "Recommendation Id", "Subscription Id", "Subscription Name", "Resource Group", "Name", "Id", "Param1", "Param2", "Param3", "Param4", "Param5", "Learn"}

rows := [][]string{}
for _, r := range rd.AprlData {
for _, r := range rd.Aprl {
row := []string{
"Azure Resource Graph",
r.Source,
Expand All @@ -132,7 +133,7 @@ func (rd *ReportData) ImpactedTable() [][]string {
rows = append(rows, row)
}

for _, d := range rd.AzqrData {
for _, d := range rd.Azqr {
for _, r := range d.Recommendations {
if r.NotCompliant {
row := []string{
Expand Down Expand Up @@ -168,10 +169,10 @@ func (rd *ReportData) CostTable() [][]string {
headers := []string{"From", "To", "Subscription", "Subscription Name", "ServiceName", "Value", "Currency"}

rows := [][]string{}
for _, r := range rd.CostData.Items {
for _, r := range rd.Cost.Items {
row := []string{
rd.CostData.From.Format("2006-01-02"),
rd.CostData.To.Format("2006-01-02"),
rd.Cost.From.Format("2006-01-02"),
rd.Cost.To.Format("2006-01-02"),
MaskSubscriptionID(r.SubscriptionID, rd.Mask),
r.SubscriptionName,
r.ServiceName,
Expand All @@ -188,7 +189,7 @@ func (rd *ReportData) CostTable() [][]string {
func (rd *ReportData) DefenderTable() [][]string {
headers := []string{"Subscription", "Subscription Name", "Name", "Tier", "Deprecated"}
rows := [][]string{}
for _, d := range rd.DefenderData {
for _, d := range rd.Defender {
row := []string{
MaskSubscriptionID(d.SubscriptionID, rd.Mask),
d.SubscriptionName,
Expand All @@ -206,7 +207,7 @@ func (rd *ReportData) DefenderTable() [][]string {
func (rd *ReportData) AdvisorTable() [][]string {
headers := []string{"Subscription", "Subscription Name", "Type", "Name", "Category", "Impact", "Description", "ResourceID", "RecommendationID"}
rows := [][]string{}
for _, d := range rd.AdvisorData {
for _, d := range rd.Advisor {
row := []string{
MaskSubscriptionID(d.SubscriptionID, rd.Mask),
d.SubscriptionName,
Expand All @@ -233,11 +234,11 @@ func (rd *ReportData) RecommendationsTable() [][]string {
}
}

for _, r := range rd.AprlData {
for _, r := range rd.Aprl {
counter[r.RecommendationID]++
}

for _, d := range rd.AzqrData {
for _, d := range rd.Azqr {
for _, r := range d.Recommendations {
if r.NotCompliant {
counter[r.RecommendationID]++
Expand Down Expand Up @@ -308,6 +309,31 @@ func (rd *ReportData) ResourceTypesTable() [][]string {
return rows
}

func (rd *ReportData) DefenderRecommendationsTable() [][]string {
headers := []string{"SubscriptionId", "SubscriptionName", "ResourceGroupName", "ResourceType", "ResourceName", "Category", "RecommendationSeverity", "RecommendationName", "ActionDescription", "RemediationDescription", "AzPortalLink", "ResourceId"}
rows := [][]string{}
for _, d := range rd.DefenderRecommendations {
row := []string{
MaskSubscriptionID(d.SubscriptionId, rd.Mask),
d.SubscriptionName,
d.ResourceGroupName,
d.ResourceType,
d.ResourceName,
d.Category,
d.RecommendationSeverity,
d.RecommendationName,
d.ActionDescription,
d.RemediationDescription,
d.AzPortalLink,
MaskSubscriptionIDInResourceID(d.ResourceId, rd.Mask),
}
rows = append(rows, row)
}

rows = append([][]string{headers}, rows...)
return rows
}

func (rd *ReportData) ResourceIDs() []*string {
ids := []*string{}
for _, r := range rd.Resources {
Expand All @@ -319,14 +345,15 @@ func (rd *ReportData) ResourceIDs() []*string {

func NewReportData(outputFile string, mask bool) ReportData {
return ReportData{
OutputFileName: outputFile,
Mask: mask,
Recomendations: map[string]map[string]azqr.AprlRecommendation{},
AzqrData: []azqr.AzqrServiceResult{},
AprlData: []azqr.AprlResult{},
DefenderData: []scanners.DefenderResult{},
AdvisorData: []scanners.AdvisorResult{},
CostData: &scanners.CostResult{
OutputFileName: outputFile,
Mask: mask,
Recomendations: map[string]map[string]azqr.AprlRecommendation{},
Azqr: []azqr.AzqrServiceResult{},
Aprl: []azqr.AprlResult{},
Defender: []scanners.DefenderResult{},
DefenderRecommendations: []azqr.DefenderRecommendation{},
Advisor: []scanners.AdvisorResult{},
Cost: &scanners.CostResult{
Items: []*scanners.CostResultItem{},
},
ResourceTypeCount: []azqr.ResourceTypeCount{},
Expand Down
Loading

0 comments on commit d730d73

Please sign in to comment.