Skip to content

Commit 6d0f741

Browse files
authored
116 lint intrf+first (#675)
lint infrastructure and first lint checker
1 parent b776830 commit 6d0f741

16 files changed

+475
-13
lines changed

cmd/analyzer/subcmds/lint.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2023- IBM Inc. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package subcmds
8+
9+
import (
10+
"github.com/spf13/cobra"
11+
12+
"github.com/np-guard/vpc-network-config-analyzer/pkg/linter"
13+
)
14+
15+
func NewLintCommand(args *inArgs) *cobra.Command {
16+
cmd := &cobra.Command{
17+
Use: "lint",
18+
Short: "Run various checks for ensuring best-practices",
19+
Long: `Execute various (configurable) linting and provides findings`,
20+
Args: cobra.NoArgs,
21+
RunE: func(cmd *cobra.Command, _ []string) error {
22+
return lintVPCConfigs(cmd, args)
23+
},
24+
}
25+
return cmd
26+
}
27+
28+
func lintVPCConfigs(cmd *cobra.Command, inArgs *inArgs) error {
29+
cmd.SilenceUsage = true // if we got this far, flags are syntactically correct, so no need to print usage
30+
cmd.SilenceErrors = true // also, error will be printed to logger in main(), so no need for cobra to also print it
31+
32+
multiConfigs, err1 := buildConfigs(inArgs)
33+
if err1 != nil {
34+
return err1
35+
}
36+
_, _, err2 := linter.LinterExecute(multiConfigs.Configs())
37+
return err2
38+
}

cmd/analyzer/subcmds/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func NewRootCommand() *cobra.Command {
110110
rootCmd.AddCommand(NewReportCommand(args))
111111
rootCmd.AddCommand(NewDiffCommand(args))
112112
rootCmd.AddCommand(NewExplainCommand(args))
113+
rootCmd.AddCommand(NewLintCommand(args))
113114
rootCmd.CompletionOptions.HiddenDefaultCmd = true
114115
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) // disable help command. should use --help flag instead
115116

pkg/ibmvpc/analysis_output_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -910,9 +910,8 @@ func compareOrRegenerateOutputPerTest(t *testing.T,
910910
compareTextualResult(expectedOutputStr, actualOutput)
911911
t.Fatalf("output mismatch expected-vs-actual on test name: %s, use case: %d", tt.name, uc)
912912
}
913-
}
914-
915-
if mode == outputGeneration {
913+
} else if mode == outputGeneration {
914+
fmt.Printf("outputGeneration\n")
916915
// create or override expected output file
917916
if _, err := vpcmodel.WriteToFile(actualOutput, tt.expectedOutput[uc]); err != nil {
918917
return err
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"Firewall rules implying different connectivity for different endpoints within a subnet" issues:
2+
------------------------------------------------------------------------------------------------
3+
In VPC test-vpc1-ky, network ACL acl3-ky rule's indexed 1 splits subnet subnet3-ky (10.240.30.0/24). Splitting rule details: index: 1, direction: outbound , src: 10.240.30.0/31 , dst: 10.240.20.0/24, conn: all, action: allow
4+
In VPC test-vpc1-ky, network ACL acl3-ky rule's indexed 3 splits subnet subnet3-ky (10.240.30.0/24). Splitting rule details: index: 3, direction: inbound , src: 10.240.20.0/24 , dst: 10.240.30.0/31, conn: all, action: allow
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"Firewall rules implying different connectivity for different endpoints within a subnet" issues:
2+
------------------------------------------------------------------------------------------------
3+
In VPC test-vpc1-ky, security group sg1-ky rule's indexed 1 splits subnet subnet1-ky (10.240.10.0/24). Splitting rule details: index: 1, direction: inbound, conns: protocol: all, remote: sg1-ky (10.240.10.4/32), local: 0.0.0.0/0
4+
In VPC test-vpc1-ky, security group sg1-ky rule's indexed 3 splits subnets subnet2-ky (10.240.20.0/24), subnet3-ky (10.240.30.0/24). Splitting rule details: index: 3, direction: inbound, conns: protocol: all, remote: sg2-ky (10.240.20.4/32,10.240.30.4/32), local: 0.0.0.0/0
5+
In VPC test-vpc1-ky, security group sg1-ky rule's indexed 4 splits subnet subnet3-ky (10.240.30.0/24). Splitting rule details: index: 4, direction: inbound, conns: protocol: all, remote: sg3-ky (10.240.30.5/32,10.240.30.6/32), local: 0.0.0.0/0
6+
In VPC test-vpc1-ky, security group sg2-ky rule's indexed 4 splits subnet subnet1-ky (10.240.10.0/24). Splitting rule details: index: 4, direction: inbound, conns: protocol: all, remote: sg1-ky (10.240.10.4/32), local: 0.0.0.0/0
7+
In VPC test-vpc1-ky, security group sg2-ky rule's indexed 6 splits subnets subnet2-ky (10.240.20.0/24), subnet3-ky (10.240.30.0/24). Splitting rule details: index: 6, direction: outbound, conns: protocol: tcp, dstPorts: 1-65535, remote: sg2-ky (10.240.20.4/32,10.240.30.4/32), local: 0.0.0.0/0
8+
In VPC test-vpc1-ky, security group sg2-ky rule's indexed 7 splits subnets subnet2-ky (10.240.20.0/24), subnet3-ky (10.240.30.0/24). Splitting rule details: index: 7, direction: inbound, conns: protocol: tcp, dstPorts: 1-65535, remote: sg2-ky (10.240.20.4/32,10.240.30.4/32), local: 0.0.0.0/0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
no lint "Firewall rules implying different connectivity for different endpoints within a subnet" issues

pkg/ibmvpc/lint_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Copyright 2023- IBM Inc. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package ibmvpc
8+
9+
import (
10+
"fmt"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/np-guard/vpc-network-config-analyzer/pkg/linter"
17+
"github.com/np-guard/vpc-network-config-analyzer/pkg/vpcmodel"
18+
)
19+
20+
const lintOut = "lint_out"
21+
22+
var lintTests = []*vpcGeneralTest{
23+
{
24+
name: "basic_acl3",
25+
inputConfig: "acl_testing3",
26+
},
27+
{
28+
name: "basic_sg1",
29+
inputConfig: "sg_testing1_new",
30+
},
31+
{
32+
name: "multivpc",
33+
inputConfig: "tgw_larger_example",
34+
},
35+
}
36+
37+
func TestAllLint(t *testing.T) {
38+
// lintTests is the list of tests to run
39+
for testIdx := range lintTests {
40+
tt := lintTests[testIdx]
41+
tt.mode = outputComparison
42+
t.Run(tt.name, func(t *testing.T) {
43+
t.Parallel()
44+
tt.runLintTest(t)
45+
})
46+
}
47+
fmt.Println("done")
48+
}
49+
50+
// uncomment the function below for generating the expected output files instead of comparing
51+
/*
52+
func TestAllLintWithGeneration(t *testing.T) {
53+
// tests is the list of tests to run
54+
for testIdx := range lintTests {
55+
tt := lintTests[testIdx]
56+
tt.mode = outputGeneration
57+
t.Run(tt.name, func(t *testing.T) {
58+
t.Parallel()
59+
tt.runLintTest(t)
60+
})
61+
}
62+
fmt.Println("done")
63+
}
64+
*/
65+
func (tt *vpcGeneralTest) runLintTest(t *testing.T) {
66+
// all tests in lint mode
67+
// output use case is not significant here, but being used so that lint test can rely on existing mechanism
68+
tt.useCases = []vpcmodel.OutputUseCase{vpcmodel.AllEndpoints}
69+
// init test - set the input/output file names according to test name
70+
tt.initTest()
71+
72+
// get vpcConfigs obj from parsing + analyzing input config file
73+
vpcConfigs := getVPCConfigs(t, tt, true)
74+
75+
// generate actual output for all use cases specified for this test
76+
err := runLintTestPerUseCase(t, tt, vpcConfigs.Configs(), lintOut)
77+
require.Equal(t, tt.errPerUseCase[vpcmodel.AllEndpoints], err, "comparing actual err to expected err")
78+
for uc, outFile := range tt.actualOutput {
79+
fmt.Printf("test %s use-case %d - generated output file: %s\n", tt.name, uc, outFile)
80+
}
81+
}
82+
83+
// runExplainTestPerUseCase executes lint for the required use case and compares/generates the output
84+
func runLintTestPerUseCase(t *testing.T,
85+
tt *vpcGeneralTest,
86+
cConfigs map[string]*vpcmodel.VPCConfig,
87+
outDir string) error {
88+
// output use case is not significant here, but being used so that lint test can rely on existing mechanism
89+
initLintTestFileNames(tt, outDir)
90+
_, actualOutput, _ := linter.LinterExecute(cConfigs)
91+
if err := compareOrRegenerateOutputPerTest(t, tt.mode, actualOutput, tt, vpcmodel.AllEndpoints); err != nil {
92+
return err
93+
}
94+
return nil
95+
}
96+
97+
func initLintTestFileNames(tt *vpcGeneralTest, testDir string) {
98+
expectedFileName, actualFileName := getLintTestFileName(tt.name)
99+
// output use case is not significant here, but being used so that lint test can rely on existing mechanism
100+
tt.actualOutput[vpcmodel.AllEndpoints] = filepath.Join(getTestsDirOut(testDir), actualFileName)
101+
tt.expectedOutput[vpcmodel.AllEndpoints] = filepath.Join(getTestsDirOut(testDir), expectedFileName)
102+
}
103+
104+
// getLintTestFileName returns expected file name and actual file name, for the relevant use case
105+
func getLintTestFileName(testName string) (expectedFileName, actualFileName string) {
106+
res := testName + "_Lint"
107+
expectedFileName = res
108+
actualFileName = actualOutFilePrefix + res
109+
return expectedFileName, actualFileName
110+
}

pkg/ibmvpc/vpc.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import (
2121
)
2222

2323
const doubleTab = "\t\t"
24+
const emptyNameError = "empty name for %s indexed %d"
25+
26+
const securityGroup = "security group"
27+
const networkACL = "network ACL"
2428

2529
///////////////////////////////////////////////////////////////////////////////////////////////////
2630

@@ -477,6 +481,25 @@ func (nl *NaclLayer) ReferencedIPblocks() []*ipblock.IPBlock {
477481
return res
478482
}
479483

484+
func (nl *NaclLayer) GetRules() ([]vpcmodel.RuleOfFilter, error) {
485+
resRules := []vpcmodel.RuleOfFilter{}
486+
for naclIndx, nacl := range nl.naclList {
487+
naclRules := nacl.analyzer.egressRules
488+
naclRules = append(naclRules, nacl.analyzer.ingressRules...)
489+
if nacl.analyzer.naclResource.Name == nil {
490+
return nil, fmt.Errorf(emptyNameError, networkACL, naclIndx)
491+
}
492+
naclName := *nacl.analyzer.naclResource.Name
493+
for _, rule := range naclRules {
494+
ruleBlocks := []*ipblock.IPBlock{rule.src, rule.dst}
495+
ruleDesc, _, _, _ := nacl.analyzer.getNACLRule(rule.index)
496+
resRules = append(resRules, *vpcmodel.NewRuleOfFilter(networkACL, naclName, ruleDesc, rule.index,
497+
ruleBlocks))
498+
}
499+
}
500+
return resRules, nil
501+
}
502+
480503
func getHeaderRulesType(filter string, rType vpcmodel.RulesType) string {
481504
switch rType {
482505
case vpcmodel.NoRules:
@@ -679,6 +702,28 @@ func (sgl *SecurityGroupLayer) ReferencedIPblocks() []*ipblock.IPBlock {
679702
return res
680703
}
681704

705+
func (sgl *SecurityGroupLayer) GetRules() ([]vpcmodel.RuleOfFilter, error) {
706+
resRules := []vpcmodel.RuleOfFilter{}
707+
for sgIndx, sg := range sgl.sgList {
708+
sgRules := sg.analyzer.egressRules
709+
sgRules = append(sgRules, sg.analyzer.ingressRules...)
710+
if sg.analyzer.sgResource.Name == nil {
711+
return nil, fmt.Errorf(emptyNameError, securityGroup, sgIndx)
712+
}
713+
sgName := *sg.analyzer.sgResource.Name
714+
for _, rule := range sgRules {
715+
ruleBlocks := []*ipblock.IPBlock{rule.remote.cidr}
716+
if rule.local != nil {
717+
ruleBlocks = append(ruleBlocks, rule.local)
718+
}
719+
ruleDesc, _, _, _ := sg.analyzer.getSGRule(rule.index)
720+
resRules = append(resRules, *vpcmodel.NewRuleOfFilter(securityGroup, sgName, ruleDesc, rule.index,
721+
ruleBlocks))
722+
}
723+
}
724+
return resRules, nil
725+
}
726+
682727
type SecurityGroup struct {
683728
vpcmodel.VPCResource
684729
analyzer *SGAnalyzer
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
Copyright 2023- IBM Inc. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package linter
8+
9+
import (
10+
"fmt"
11+
"strings"
12+
13+
"github.com/np-guard/models/pkg/ipblock"
14+
"github.com/np-guard/vpc-network-config-analyzer/pkg/vpcmodel"
15+
)
16+
17+
const splitRuleSubnetName = "rules-splitting-subnets"
18+
19+
// filterRuleSplitSubnet: rules of filters that are inconsistent w.r.t. subnets.
20+
type filterRuleSplitSubnet struct {
21+
basicLinter
22+
}
23+
24+
// a rule with the list of subnets it splits
25+
type splitRuleSubnet struct {
26+
vpcName string
27+
rule vpcmodel.RuleOfFilter
28+
splitSubnets []vpcmodel.Subnet
29+
}
30+
31+
// /////////////////////////////////////////////////////////
32+
// lint interface implementation for filterRuleSplitSubnet
33+
// ////////////////////////////////////////////////////////
34+
func (lint *filterRuleSplitSubnet) lintName() string {
35+
return splitRuleSubnetName
36+
}
37+
38+
func (lint *filterRuleSplitSubnet) lintDescription() string {
39+
return "Firewall rules implying different connectivity for different endpoints within a subnet"
40+
}
41+
42+
func (lint *filterRuleSplitSubnet) check() error {
43+
for _, config := range lint.configs {
44+
if config.IsMultipleVPCsConfig {
45+
continue // no use in executing lint on dummy vpcs
46+
}
47+
for _, layer := range vpcmodel.FilterLayers {
48+
filterLayer := config.GetFilterTrafficResourceOfKind(layer)
49+
rules, err := filterLayer.GetRules()
50+
if err != nil {
51+
return err
52+
}
53+
for _, rule := range rules {
54+
subnetsSplitByRule := []vpcmodel.Subnet{}
55+
for _, subnet := range config.Subnets {
56+
splitSubnet := ruleSplitSubnet(subnet, rule.IPBlocks)
57+
if splitSubnet {
58+
subnetsSplitByRule = append(subnetsSplitByRule, subnet)
59+
}
60+
}
61+
if len(subnetsSplitByRule) > 0 {
62+
lint.addFinding(&splitRuleSubnet{vpcName: config.VPC.Name(), rule: rule,
63+
splitSubnets: subnetsSplitByRule})
64+
}
65+
}
66+
}
67+
}
68+
return nil
69+
}
70+
71+
// given a subnet and IPBlocks mentioned in a rule, returns the list
72+
func ruleSplitSubnet(subnet vpcmodel.Subnet, ruleIPBlocks []*ipblock.IPBlock) bool {
73+
subnetCidrIPBlock := subnet.AddressRange()
74+
for _, ruleIPBlock := range ruleIPBlocks {
75+
if ruleIPBlock.Overlap(subnetCidrIPBlock) && !subnetCidrIPBlock.ContainedIn(ruleIPBlock) {
76+
return true
77+
}
78+
}
79+
return false
80+
}
81+
82+
///////////////////////////////////////////////////////////
83+
// finding interface implementation for splitRuleSubnet
84+
//////////////////////////////////////////////////////////
85+
86+
func (finding *splitRuleSubnet) vpc() string {
87+
return finding.vpcName
88+
}
89+
90+
func (finding *splitRuleSubnet) string() string {
91+
rule := finding.rule
92+
subnetsStrSlice := make([]string, len(finding.splitSubnets))
93+
for i, subnet := range finding.splitSubnets {
94+
subnetsStrSlice[i] = fmt.Sprintf("%s (%s)", subnet.Name(), subnet.CIDR())
95+
}
96+
subnetStr := strings.Join(subnetsStrSlice, ", ")
97+
if len(subnetsStrSlice) > 1 {
98+
subnetStr = "subnets " + subnetStr
99+
} else {
100+
subnetStr = "subnet " + subnetStr
101+
}
102+
return fmt.Sprintf("In VPC %s, %s %s rule's indexed %d splits %s. Splitting rule details: %s",
103+
finding.vpc(), finding.rule.LayerName, rule.FilterName, rule.RuleIndex, subnetStr, rule.RuleDesc)
104+
}
105+
106+
// for json: a rule with the list of subnets it splits
107+
type splitRuleSubnetJSON struct {
108+
VpcName string `json:"vpc_name"`
109+
Rule vpcmodel.RuleOfFilter `json:"rule_details"`
110+
SplitSubnets []subnetJSON `json:"splitted_subnets"`
111+
}
112+
113+
type subnetJSON struct {
114+
Name string `json:"name"`
115+
CIDR string `json:"cidr"`
116+
}
117+
118+
func (finding *splitRuleSubnet) toJSON() any {
119+
rule := finding.rule
120+
splitSubnetsJSON := make([]subnetJSON, len(finding.splitSubnets))
121+
for i, splitSubnet := range finding.splitSubnets {
122+
splitSubnetsJSON[i] = subnetJSON{Name: splitSubnet.Name(), CIDR: splitSubnet.CIDR()}
123+
}
124+
res := splitRuleSubnetJSON{VpcName: finding.vpcName, Rule: vpcmodel.RuleOfFilter{LayerName: rule.LayerName,
125+
FilterName: rule.FilterName, RuleIndex: rule.RuleIndex, RuleDesc: rule.RuleDesc},
126+
SplitSubnets: splitSubnetsJSON}
127+
return res
128+
}

0 commit comments

Comments
 (0)