Skip to content

Commit 3303f9f

Browse files
Lionel-Wilsonactions-user
authored andcommitted
feat: implement NetworkDefaultSecurityRule adapter (#4216)
<img width="1474" height="1002" alt="image" src="https://github.com/user-attachments/assets/8b62255e-97f6-4f78-94d7-2a62b1553c48" /> <!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter and client wiring that will increase API calls and linked-query generation for NSG scans. Risk is moderate because it touches adapter initialization/registration and paging logic, but is isolated to a new resource type. > > **Overview** > Adds support for discovering Azure NSG *default* security rules by introducing a new `NetworkDefaultSecurityRule` searchable adapter, including `Get`, `Search`, and streaming search, and wiring it into the Azure manual adapter registry. > > Introduces a thin `DefaultSecurityRulesClient` wrapper (with generated GoMock) and updates resource-ID path extraction to understand `azure-network-default-security-rule`; includes a dedicated unit test suite covering paging, validation, and error cases. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 83470e2e2e56c5effbd6bbf5b777f1b1fc789bb9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> GitOrigin-RevId: 3fb0c7bdbef4b58a368deeffa2e726c9a2aaa793
1 parent de09874 commit 3303f9f

6 files changed

Lines changed: 680 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package clients
2+
3+
import (
4+
"context"
5+
6+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9"
7+
)
8+
9+
//go:generate mockgen -destination=../shared/mocks/mock_default_security_rules_client.go -package=mocks -source=default-security-rules-client.go
10+
11+
// DefaultSecurityRulesPager is a type alias for the generic Pager interface with default security rules list response type.
12+
type DefaultSecurityRulesPager = Pager[armnetwork.DefaultSecurityRulesClientListResponse]
13+
14+
// DefaultSecurityRulesClient is an interface for interacting with Azure NSG default security rules (child of network security group).
15+
type DefaultSecurityRulesClient interface {
16+
Get(ctx context.Context, resourceGroupName string, networkSecurityGroupName string, defaultSecurityRuleName string, options *armnetwork.DefaultSecurityRulesClientGetOptions) (armnetwork.DefaultSecurityRulesClientGetResponse, error)
17+
NewListPager(resourceGroupName string, networkSecurityGroupName string, options *armnetwork.DefaultSecurityRulesClientListOptions) DefaultSecurityRulesPager
18+
}
19+
20+
type defaultSecurityRulesClient struct {
21+
client *armnetwork.DefaultSecurityRulesClient
22+
}
23+
24+
func (c *defaultSecurityRulesClient) Get(ctx context.Context, resourceGroupName string, networkSecurityGroupName string, defaultSecurityRuleName string, options *armnetwork.DefaultSecurityRulesClientGetOptions) (armnetwork.DefaultSecurityRulesClientGetResponse, error) {
25+
return c.client.Get(ctx, resourceGroupName, networkSecurityGroupName, defaultSecurityRuleName, options)
26+
}
27+
28+
func (c *defaultSecurityRulesClient) NewListPager(resourceGroupName string, networkSecurityGroupName string, options *armnetwork.DefaultSecurityRulesClientListOptions) DefaultSecurityRulesPager {
29+
return c.client.NewListPager(resourceGroupName, networkSecurityGroupName, options)
30+
}
31+
32+
// NewDefaultSecurityRulesClient creates a new DefaultSecurityRulesClient from the Azure SDK client.
33+
func NewDefaultSecurityRulesClient(client *armnetwork.DefaultSecurityRulesClient) DefaultSecurityRulesClient {
34+
return &defaultSecurityRulesClient{client: client}
35+
}

sources/azure/manual/adapters.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred
237237
return nil, fmt.Errorf("failed to create security rules client: %w", err)
238238
}
239239

240+
defaultSecurityRulesClient, err := armnetwork.NewDefaultSecurityRulesClient(subscriptionID, cred, nil)
241+
if err != nil {
242+
return nil, fmt.Errorf("failed to create default security rules client: %w", err)
243+
}
244+
240245
applicationGatewaysClient, err := armnetwork.NewApplicationGatewaysClient(subscriptionID, cred, nil)
241246
if err != nil {
242247
return nil, fmt.Errorf("failed to create application gateways client: %w", err)
@@ -594,6 +599,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred
594599
clients.NewSecurityRulesClient(securityRulesClient),
595600
resourceGroupScopes,
596601
), cache),
602+
sources.WrapperToAdapter(NewNetworkDefaultSecurityRule(
603+
clients.NewDefaultSecurityRulesClient(defaultSecurityRulesClient),
604+
resourceGroupScopes,
605+
), cache),
597606
sources.WrapperToAdapter(NewNetworkApplicationGateway(
598607
clients.NewApplicationGatewaysClient(applicationGatewaysClient),
599608
resourceGroupScopes,
@@ -763,6 +772,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred
763772
sources.WrapperToAdapter(NewNetworkNetworkSecurityGroup(nil, placeholderResourceGroupScopes), noOpCache),
764773
sources.WrapperToAdapter(NewNetworkApplicationSecurityGroup(nil, placeholderResourceGroupScopes), noOpCache),
765774
sources.WrapperToAdapter(NewNetworkSecurityRule(nil, placeholderResourceGroupScopes), noOpCache),
775+
sources.WrapperToAdapter(NewNetworkDefaultSecurityRule(nil, placeholderResourceGroupScopes), noOpCache),
766776
sources.WrapperToAdapter(NewNetworkRouteTable(nil, placeholderResourceGroupScopes), noOpCache),
767777
sources.WrapperToAdapter(NewNetworkApplicationGateway(nil, placeholderResourceGroupScopes), noOpCache),
768778
sources.WrapperToAdapter(NewNetworkVirtualNetworkGateway(nil, placeholderResourceGroupScopes), noOpCache),
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package manual
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9"
8+
"github.com/overmindtech/cli/go/discovery"
9+
"github.com/overmindtech/cli/go/sdp-go"
10+
"github.com/overmindtech/cli/go/sdpcache"
11+
"github.com/overmindtech/cli/sources"
12+
"github.com/overmindtech/cli/sources/azure/clients"
13+
azureshared "github.com/overmindtech/cli/sources/azure/shared"
14+
"github.com/overmindtech/cli/sources/shared"
15+
"github.com/overmindtech/cli/sources/stdlib"
16+
)
17+
18+
var NetworkDefaultSecurityRuleLookupByUniqueAttr = shared.NewItemTypeLookup("uniqueAttr", azureshared.NetworkDefaultSecurityRule)
19+
20+
type networkDefaultSecurityRuleWrapper struct {
21+
client clients.DefaultSecurityRulesClient
22+
*azureshared.MultiResourceGroupBase
23+
}
24+
25+
// NewNetworkDefaultSecurityRule creates a new networkDefaultSecurityRuleWrapper instance (SearchableWrapper: child of network security group).
26+
func NewNetworkDefaultSecurityRule(client clients.DefaultSecurityRulesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper {
27+
return &networkDefaultSecurityRuleWrapper{
28+
client: client,
29+
MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase(
30+
resourceGroupScopes,
31+
sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK,
32+
azureshared.NetworkDefaultSecurityRule,
33+
),
34+
}
35+
}
36+
37+
func (n networkDefaultSecurityRuleWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) {
38+
if len(queryParts) < 2 {
39+
return nil, &sdp.QueryError{
40+
ErrorType: sdp.QueryError_OTHER,
41+
ErrorString: "Get requires 2 query parts: networkSecurityGroupName and defaultSecurityRuleName",
42+
Scope: scope,
43+
ItemType: n.Type(),
44+
}
45+
}
46+
nsgName := queryParts[0]
47+
ruleName := queryParts[1]
48+
if ruleName == "" {
49+
return nil, azureshared.QueryError(errors.New("default security rule name cannot be empty"), scope, n.Type())
50+
}
51+
52+
rgScope, err := n.ResourceGroupScopeFromScope(scope)
53+
if err != nil {
54+
return nil, azureshared.QueryError(err, scope, n.Type())
55+
}
56+
resp, err := n.client.Get(ctx, rgScope.ResourceGroup, nsgName, ruleName, nil)
57+
if err != nil {
58+
return nil, azureshared.QueryError(err, scope, n.Type())
59+
}
60+
61+
return n.azureDefaultSecurityRuleToSDPItem(&resp.SecurityRule, nsgName, ruleName, scope)
62+
}
63+
64+
func (n networkDefaultSecurityRuleWrapper) GetLookups() sources.ItemTypeLookups {
65+
return sources.ItemTypeLookups{
66+
NetworkNetworkSecurityGroupLookupByName,
67+
NetworkDefaultSecurityRuleLookupByUniqueAttr,
68+
}
69+
}
70+
71+
func (n networkDefaultSecurityRuleWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) {
72+
if len(queryParts) < 1 {
73+
return nil, &sdp.QueryError{
74+
ErrorType: sdp.QueryError_OTHER,
75+
ErrorString: "Search requires 1 query part: networkSecurityGroupName",
76+
Scope: scope,
77+
ItemType: n.Type(),
78+
}
79+
}
80+
nsgName := queryParts[0]
81+
82+
rgScope, err := n.ResourceGroupScopeFromScope(scope)
83+
if err != nil {
84+
return nil, azureshared.QueryError(err, scope, n.Type())
85+
}
86+
pager := n.client.NewListPager(rgScope.ResourceGroup, nsgName, nil)
87+
88+
var items []*sdp.Item
89+
for pager.More() {
90+
page, err := pager.NextPage(ctx)
91+
if err != nil {
92+
return nil, azureshared.QueryError(err, scope, n.Type())
93+
}
94+
for _, rule := range page.Value {
95+
if rule == nil || rule.Name == nil {
96+
continue
97+
}
98+
item, sdpErr := n.azureDefaultSecurityRuleToSDPItem(rule, nsgName, *rule.Name, scope)
99+
if sdpErr != nil {
100+
return nil, sdpErr
101+
}
102+
items = append(items, item)
103+
}
104+
}
105+
return items, nil
106+
}
107+
108+
func (n networkDefaultSecurityRuleWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) {
109+
if len(queryParts) < 1 {
110+
stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: networkSecurityGroupName"), scope, n.Type()))
111+
return
112+
}
113+
nsgName := queryParts[0]
114+
115+
rgScope, err := n.ResourceGroupScopeFromScope(scope)
116+
if err != nil {
117+
stream.SendError(azureshared.QueryError(err, scope, n.Type()))
118+
return
119+
}
120+
pager := n.client.NewListPager(rgScope.ResourceGroup, nsgName, nil)
121+
for pager.More() {
122+
page, err := pager.NextPage(ctx)
123+
if err != nil {
124+
stream.SendError(azureshared.QueryError(err, scope, n.Type()))
125+
return
126+
}
127+
for _, rule := range page.Value {
128+
if rule == nil || rule.Name == nil {
129+
continue
130+
}
131+
item, sdpErr := n.azureDefaultSecurityRuleToSDPItem(rule, nsgName, *rule.Name, scope)
132+
if sdpErr != nil {
133+
stream.SendError(sdpErr)
134+
continue
135+
}
136+
cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey)
137+
stream.SendItem(item)
138+
}
139+
}
140+
}
141+
142+
func (n networkDefaultSecurityRuleWrapper) SearchLookups() []sources.ItemTypeLookups {
143+
return []sources.ItemTypeLookups{
144+
{NetworkNetworkSecurityGroupLookupByName},
145+
}
146+
}
147+
148+
func (n networkDefaultSecurityRuleWrapper) azureDefaultSecurityRuleToSDPItem(rule *armnetwork.SecurityRule, nsgName, ruleName, scope string) (*sdp.Item, *sdp.QueryError) {
149+
attributes, err := shared.ToAttributesWithExclude(rule, "tags")
150+
if err != nil {
151+
return nil, azureshared.QueryError(err, scope, n.Type())
152+
}
153+
154+
err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(nsgName, ruleName))
155+
if err != nil {
156+
return nil, azureshared.QueryError(err, scope, n.Type())
157+
}
158+
159+
sdpItem := &sdp.Item{
160+
Type: azureshared.NetworkDefaultSecurityRule.String(),
161+
UniqueAttribute: "uniqueAttr",
162+
Attributes: attributes,
163+
Scope: scope,
164+
}
165+
166+
// Link to parent Network Security Group
167+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
168+
Query: &sdp.Query{
169+
Type: azureshared.NetworkNetworkSecurityGroup.String(),
170+
Method: sdp.QueryMethod_GET,
171+
Query: nsgName,
172+
Scope: scope,
173+
},
174+
})
175+
176+
if rule.Properties != nil {
177+
// Link to SourceApplicationSecurityGroups
178+
if rule.Properties.SourceApplicationSecurityGroups != nil {
179+
for _, asgRef := range rule.Properties.SourceApplicationSecurityGroups {
180+
if asgRef != nil && asgRef.ID != nil {
181+
asgName := azureshared.ExtractResourceName(*asgRef.ID)
182+
if asgName != "" {
183+
linkScope := scope
184+
if extractedScope := azureshared.ExtractScopeFromResourceID(*asgRef.ID); extractedScope != "" {
185+
linkScope = extractedScope
186+
}
187+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
188+
Query: &sdp.Query{
189+
Type: azureshared.NetworkApplicationSecurityGroup.String(),
190+
Method: sdp.QueryMethod_GET,
191+
Query: asgName,
192+
Scope: linkScope,
193+
},
194+
})
195+
}
196+
}
197+
}
198+
}
199+
200+
// Link to DestinationApplicationSecurityGroups
201+
if rule.Properties.DestinationApplicationSecurityGroups != nil {
202+
for _, asgRef := range rule.Properties.DestinationApplicationSecurityGroups {
203+
if asgRef != nil && asgRef.ID != nil {
204+
asgName := azureshared.ExtractResourceName(*asgRef.ID)
205+
if asgName != "" {
206+
linkScope := scope
207+
if extractedScope := azureshared.ExtractScopeFromResourceID(*asgRef.ID); extractedScope != "" {
208+
linkScope = extractedScope
209+
}
210+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
211+
Query: &sdp.Query{
212+
Type: azureshared.NetworkApplicationSecurityGroup.String(),
213+
Method: sdp.QueryMethod_GET,
214+
Query: asgName,
215+
Scope: linkScope,
216+
},
217+
})
218+
}
219+
}
220+
}
221+
}
222+
223+
// Link to stdlib.NetworkIP for source/destination address prefixes when they are IPs or CIDRs
224+
if rule.Properties.SourceAddressPrefix != nil {
225+
appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *rule.Properties.SourceAddressPrefix)
226+
}
227+
for _, p := range rule.Properties.SourceAddressPrefixes {
228+
if p != nil {
229+
appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p)
230+
}
231+
}
232+
if rule.Properties.DestinationAddressPrefix != nil {
233+
appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *rule.Properties.DestinationAddressPrefix)
234+
}
235+
for _, p := range rule.Properties.DestinationAddressPrefixes {
236+
if p != nil {
237+
appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p)
238+
}
239+
}
240+
}
241+
242+
return sdpItem, nil
243+
}
244+
245+
func (n networkDefaultSecurityRuleWrapper) PotentialLinks() map[shared.ItemType]bool {
246+
return shared.NewItemTypesSet(
247+
azureshared.NetworkNetworkSecurityGroup,
248+
azureshared.NetworkApplicationSecurityGroup,
249+
stdlib.NetworkIP,
250+
)
251+
}
252+
253+
// ref: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-security-groups/get#defaultsecurityrules
254+
func (n networkDefaultSecurityRuleWrapper) IAMPermissions() []string {
255+
return []string{
256+
"Microsoft.Network/networkSecurityGroups/defaultSecurityRules/read",
257+
}
258+
}
259+
260+
func (n networkDefaultSecurityRuleWrapper) PredefinedRole() string {
261+
return "Reader"
262+
}

0 commit comments

Comments
 (0)