Skip to content

116 lint intrf+first #675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 69 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
bf4aad8
basic lint infrastructure
ShiriMoran Jul 3, 2024
25cf656
lint for filters rules splitting subnets - defining structs
ShiriMoran Jul 3, 2024
ed695b3
added comments
ShiriMoran Jul 3, 2024
0d5a121
fix typo
ShiriMoran Jul 3, 2024
3adf6ac
added relevant func def to interface of FilterTrafficResource
ShiriMoran Jul 4, 2024
e791e9b
implemented interface to get rules details
ShiriMoran Jul 4, 2024
d0bcda8
first lint implemented
ShiriMoran Jul 4, 2024
9f32da6
tests for lint - not checked yet
ShiriMoran Jul 7, 2024
4340cfb
typos fixed (still not working)
ShiriMoran Jul 7, 2024
4818c11
finalizes 1st lint testing
ShiriMoran Jul 7, 2024
9f95194
finalizes 1st lint testing
ShiriMoran Jul 7, 2024
19f0a88
lint batch 1
ShiriMoran Jul 7, 2024
67641b9
typo fix in tests
ShiriMoran Jul 7, 2024
75b37a3
lint
ShiriMoran Jul 7, 2024
ab86a64
lint
ShiriMoran Jul 7, 2024
84eaa7b
lint
ShiriMoran Jul 7, 2024
281e72e
typo fix
ShiriMoran Jul 7, 2024
b9d0b12
lint
ShiriMoran Jul 7, 2024
94e504f
lint
ShiriMoran Jul 7, 2024
7c13d5d
getFindings()
ShiriMoran Jul 7, 2024
dc4a984
cosmetic
ShiriMoran Jul 7, 2024
6063eaf
printing improvements
ShiriMoran Jul 8, 2024
7e33a2b
towards full support in multiVPC
ShiriMoran Jul 8, 2024
3226f8d
support in multiVPC
ShiriMoran Jul 8, 2024
acc15cd
support in multiVPC
ShiriMoran Jul 8, 2024
74f3936
have deterministic output result
ShiriMoran Jul 8, 2024
a8275ed
have deterministic output result
ShiriMoran Jul 8, 2024
17bc9cb
multiVPC - added vpcUID to finding
ShiriMoran Jul 8, 2024
68fff4f
multiVPC - added vpcUID to finding
ShiriMoran Jul 8, 2024
1389d71
lint
ShiriMoran Jul 8, 2024
9b35898
printing improve
ShiriMoran Jul 8, 2024
a4c71ce
lint
ShiriMoran Jul 8, 2024
7f4b9b2
Merge branch 'main' into 116_lint_intrf+first
ShiriMoran Jul 8, 2024
614beaa
Merge branch 'main' into 116_lint_intrf+first
ShiriMoran Jul 8, 2024
f821d56
CR
ShiriMoran Jul 8, 2024
f00f046
Update cmd/analyzer/subcmds/lint.go
ShiriMoran Jul 8, 2024
ee98502
CR: finding interface, add getDescription() to linter interface
ShiriMoran Jul 8, 2024
f43c560
lint
ShiriMoran Jul 8, 2024
bf411a7
lint
ShiriMoran Jul 8, 2024
311a807
lint
ShiriMoran Jul 8, 2024
ef44270
Merge remote-tracking branch 'origin/116_lint_intrf+first' into 116_l…
ShiriMoran Jul 8, 2024
0f9ecb0
redefine interface
ShiriMoran Jul 9, 2024
e5b065a
fixed tests
ShiriMoran Jul 9, 2024
67aed51
lint
ShiriMoran Jul 9, 2024
e3dc90c
Merge branch 'main' into 116_lint_intrf+first
ShiriMoran Jul 9, 2024
4b53121
comment
ShiriMoran Jul 9, 2024
9ee9329
CR
ShiriMoran Jul 9, 2024
56a9c47
exporting functionality
ShiriMoran Jul 9, 2024
02b1008
CR: specific linters are internal
ShiriMoran Jul 10, 2024
167a8a1
CR: specific linters are internal
ShiriMoran Jul 10, 2024
7ebbe1a
CR: add standard addFinding(f finding) and getFindings()
ShiriMoran Jul 10, 2024
afbb01e
CR:impl only once getFindings()
ShiriMoran Jul 10, 2024
c200190
CR:impl only once string() of linter
ShiriMoran Jul 10, 2024
ba96424
lint
ShiriMoran Jul 10, 2024
cf6ae7e
toJSON implemented, still not checked
ShiriMoran Jul 10, 2024
d633a91
lint
ShiriMoran Jul 11, 2024
bec59f1
lint
ShiriMoran Jul 11, 2024
708b354
Merge branch 'main' into 116_lint_intrf+first
ShiriMoran Jul 11, 2024
340c5b9
fix manually merge with main
ShiriMoran Jul 11, 2024
475aa2d
Merge branch 'main' into 116_lint_intrf+first
ShiriMoran Jul 11, 2024
cc0106d
CR fix typo
ShiriMoran Jul 11, 2024
cae9a91
Merge remote-tracking branch 'origin/116_lint_intrf+first' into 116_l…
ShiriMoran Jul 11, 2024
e5640d2
CR add documentation
ShiriMoran Jul 11, 2024
8d0aa6b
CR RuleIndx -> RuleIndex.
ShiriMoran Jul 11, 2024
90a6081
Merge branch 'main' into 116_lint_intrf+first
ShiriMoran Jul 11, 2024
c1911d9
CR
ShiriMoran Jul 11, 2024
1189ec4
Merge remote-tracking branch 'origin/116_lint_intrf+first' into 116_l…
ShiriMoran Jul 11, 2024
a3e1ab9
lint
ShiriMoran Jul 11, 2024
2994329
Merge branch 'main' into 116_lint_intrf+first
ShiriMoran Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions cmd/analyzer/subcmds/lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2023- IBM Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package subcmds

import (
"github.com/spf13/cobra"

"github.com/np-guard/vpc-network-config-analyzer/pkg/linter"
)

func NewLintCommand(args *inArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "lint",
Short: "Run various checks for ensuring best-practices",
Long: `Execute various (configurable) linting and provides findings`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return lintVPCConfigs(cmd, args)
},
}
return cmd
}

func lintVPCConfigs(cmd *cobra.Command, inArgs *inArgs) error {
cmd.SilenceUsage = true // if we got this far, flags are syntactically correct, so no need to print usage
cmd.SilenceErrors = true // also, error will be printed to logger in main(), so no need for cobra to also print it

multiConfigs, err1 := buildConfigs(inArgs)
if err1 != nil {
return err1
}
_, _, err2 := linter.LinterExecute(multiConfigs.Configs())
return err2
}
1 change: 1 addition & 0 deletions cmd/analyzer/subcmds/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func NewRootCommand() *cobra.Command {
rootCmd.AddCommand(NewReportCommand(args))
rootCmd.AddCommand(NewDiffCommand(args))
rootCmd.AddCommand(NewExplainCommand(args))
rootCmd.AddCommand(NewLintCommand(args))
rootCmd.CompletionOptions.HiddenDefaultCmd = true
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) // disable help command. should use --help flag instead

Expand Down
5 changes: 2 additions & 3 deletions pkg/ibmvpc/analysis_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -910,9 +910,8 @@ func compareOrRegenerateOutputPerTest(t *testing.T,
compareTextualResult(expectedOutputStr, actualOutput)
t.Fatalf("output mismatch expected-vs-actual on test name: %s, use case: %d", tt.name, uc)
}
}

if mode == outputGeneration {
} else if mode == outputGeneration {
fmt.Printf("outputGeneration\n")
// create or override expected output file
if _, err := vpcmodel.WriteToFile(actualOutput, tt.expectedOutput[uc]); err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions pkg/ibmvpc/examples/out/lint_out/basic_acl3_Lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"Firewall rules implying different connectivity for different endpoints within a subnet" issues:
------------------------------------------------------------------------------------------------
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
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
8 changes: 8 additions & 0 deletions pkg/ibmvpc/examples/out/lint_out/basic_sg1_Lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"Firewall rules implying different connectivity for different endpoints within a subnet" issues:
------------------------------------------------------------------------------------------------
In VPC test-vpc1-ky, Network acl 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
In VPC test-vpc1-ky, Network acl 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
In VPC test-vpc1-ky, Network acl 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
In VPC test-vpc1-ky, Network acl 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
In VPC test-vpc1-ky, Network acl 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
In VPC test-vpc1-ky, Network acl 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
1 change: 1 addition & 0 deletions pkg/ibmvpc/examples/out/lint_out/multivpc_Lint
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
no lint "Firewall rules implying different connectivity for different endpoints within a subnet" issues
110 changes: 110 additions & 0 deletions pkg/ibmvpc/lint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright 2023- IBM Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package ibmvpc

import (
"fmt"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/np-guard/vpc-network-config-analyzer/pkg/linter"
"github.com/np-guard/vpc-network-config-analyzer/pkg/vpcmodel"
)

const lintOut = "lint_out"

var lintTests = []*vpcGeneralTest{
{
name: "basic_acl3",
inputConfig: "acl_testing3",
},
{
name: "basic_sg1",
inputConfig: "sg_testing1_new",
},
{
name: "multivpc",
inputConfig: "tgw_larger_example",
},
}

func TestAllLint(t *testing.T) {
// lintTests is the list of tests to run
for testIdx := range lintTests {
tt := lintTests[testIdx]
tt.mode = outputComparison
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tt.runLintTest(t)
})
}
fmt.Println("done")
}

// uncomment the function below for generating the expected output files instead of comparing
/*
func TestAllLintWithGeneration(t *testing.T) {
// tests is the list of tests to run
for testIdx := range lintTests {
tt := lintTests[testIdx]
tt.mode = outputGeneration
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tt.runLintTest(t)
})
}
fmt.Println("done")
}
*/
func (tt *vpcGeneralTest) runLintTest(t *testing.T) {
// all tests in lint mode
// output use case is not significant here, but being used so that lint test can rely on existing mechanism
tt.useCases = []vpcmodel.OutputUseCase{vpcmodel.AllEndpoints}
// init test - set the input/output file names according to test name
tt.initTest()

// get vpcConfigs obj from parsing + analyzing input config file
vpcConfigs := getVPCConfigs(t, tt, true)

// generate actual output for all use cases specified for this test
err := runLintTestPerUseCase(t, tt, vpcConfigs.Configs(), lintOut)
require.Equal(t, tt.errPerUseCase[vpcmodel.AllEndpoints], err, "comparing actual err to expected err")
for uc, outFile := range tt.actualOutput {
fmt.Printf("test %s use-case %d - generated output file: %s\n", tt.name, uc, outFile)
}
}

// runExplainTestPerUseCase executes lint for the required use case and compares/generates the output
func runLintTestPerUseCase(t *testing.T,
tt *vpcGeneralTest,
cConfigs map[string]*vpcmodel.VPCConfig,
outDir string) error {
// output use case is not significant here, but being used so that lint test can rely on existing mechanism
initLintTestFileNames(tt, outDir)
_, actualOutput, _ := linter.LinterExecute(cConfigs)
if err := compareOrRegenerateOutputPerTest(t, tt.mode, actualOutput, tt, vpcmodel.AllEndpoints); err != nil {
return err
}
return nil
}

func initLintTestFileNames(tt *vpcGeneralTest, testDir string) {
expectedFileName, actualFileName := getLintTestFileName(tt.name)
// output use case is not significant here, but being used so that lint test can rely on existing mechanism
tt.actualOutput[vpcmodel.AllEndpoints] = filepath.Join(getTestsDirOut(testDir), actualFileName)
tt.expectedOutput[vpcmodel.AllEndpoints] = filepath.Join(getTestsDirOut(testDir), expectedFileName)
}

// getLintTestFileName returns expected file name and actual file name, for the relevant use case
func getLintTestFileName(testName string) (expectedFileName, actualFileName string) {
res := testName + "_Lint"
expectedFileName = res
actualFileName = actualOutFilePrefix + res
return expectedFileName, actualFileName
}
45 changes: 45 additions & 0 deletions pkg/ibmvpc/vpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import (
)

const doubleTab = "\t\t"
const emptyNameError = "empty name for %s indexed %d"

const securityGroup = "security group"
const networkACL = "network ACL"

///////////////////////////////////////////////////////////////////////////////////////////////////

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

func (nl *NaclLayer) GetRules() ([]vpcmodel.RuleOfFilter, error) {
resRules := []vpcmodel.RuleOfFilter{}
for naclIndx, nacl := range nl.naclList {
naclRules := nacl.analyzer.egressRules
naclRules = append(naclRules, nacl.analyzer.ingressRules...)
if nacl.analyzer.naclResource.Name == nil {
return nil, fmt.Errorf(emptyNameError, networkACL, naclIndx)
}
naclName := *nacl.analyzer.naclResource.Name
for _, rule := range naclRules {
ruleBlocks := []*ipblock.IPBlock{rule.src, rule.dst}
ruleDesc, _, _, _ := nacl.analyzer.getNACLRule(rule.index)
resRules = append(resRules, *vpcmodel.NewRuleOfFilter(networkACL, naclName, ruleDesc, rule.index,
ruleBlocks))
}
}
return resRules, nil
}

func getHeaderRulesType(filter string, rType vpcmodel.RulesType) string {
switch rType {
case vpcmodel.NoRules:
Expand Down Expand Up @@ -679,6 +702,28 @@ func (sgl *SecurityGroupLayer) ReferencedIPblocks() []*ipblock.IPBlock {
return res
}

func (sgl *SecurityGroupLayer) GetRules() ([]vpcmodel.RuleOfFilter, error) {
resRules := []vpcmodel.RuleOfFilter{}
for sgIndx, sg := range sgl.sgList {
sgRules := sg.analyzer.egressRules
sgRules = append(sgRules, sg.analyzer.ingressRules...)
if sg.analyzer.sgResource.Name == nil {
return nil, fmt.Errorf(emptyNameError, securityGroup, sgIndx)
}
sgName := *sg.analyzer.sgResource.Name
for _, rule := range sgRules {
ruleBlocks := []*ipblock.IPBlock{rule.remote.cidr}
if rule.local != nil {
ruleBlocks = append(ruleBlocks, rule.local)
}
ruleDesc, _, _, _ := sg.analyzer.getSGRule(rule.index)
resRules = append(resRules, *vpcmodel.NewRuleOfFilter(securityGroup, sgName, ruleDesc, rule.index,
ruleBlocks))
}
}
return resRules, nil
}

type SecurityGroup struct {
vpcmodel.VPCResource
analyzer *SGAnalyzer
Expand Down
139 changes: 139 additions & 0 deletions pkg/linter/lintFilterRuleSplitSubnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
Copyright 2023- IBM Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package linter

import (
"fmt"
"strings"

"github.com/np-guard/models/pkg/ipblock"
"github.com/np-guard/vpc-network-config-analyzer/pkg/vpcmodel"
)

const splitRuleSubnetName = "rules-splitting-subnets"

// filterRuleSplitSubnet: rules of filters that are inconsistent w.r.t. subnets.
type filterRuleSplitSubnet struct {
basicLinter
}

// a rule with the list of subnets it splits
type splitRuleSubnet struct {
vpcName string
rule vpcmodel.RuleOfFilter
splitSubnets []vpcmodel.Subnet
}

// /////////////////////////////////////////////////////////
// lint interface implementation for filterRuleSplitSubnet
// ////////////////////////////////////////////////////////
func (lint *filterRuleSplitSubnet) lintName() string {
return splitRuleSubnetName
}

func (lint *filterRuleSplitSubnet) lintDescription() string {
return "Firewall rules implying different connectivity for different endpoints within a subnet"
}

func (lint *filterRuleSplitSubnet) check() error {
for _, config := range lint.configs {
if config.IsMultipleVPCsConfig {
continue // no use in executing lint on dummy vpcs
}
for _, layer := range vpcmodel.FilterLayers {
filterLayer := config.GetFilterTrafficResourceOfKind(layer)
rules, err := filterLayer.GetRules()
if err != nil {
return err
}
for _, rule := range rules {
subnetsSplitByRule := []vpcmodel.Subnet{}
for _, subnet := range config.Subnets {
splitSubnet, err := ruleSplitSubnet(subnet, rule.IPBlocks)
if err != nil {
return err
}
if splitSubnet {
subnetsSplitByRule = append(subnetsSplitByRule, subnet)
}
}
if len(subnetsSplitByRule) > 0 {
lint.addFinding(&splitRuleSubnet{vpcName: config.VPC.Name(), rule: rule,
splitSubnets: subnetsSplitByRule})
}
}
}
}
return nil
}

// given a subnet and IPBlocks mentioned in a rule, returns the list
func ruleSplitSubnet(subnet vpcmodel.Subnet, ruleIPBlocks []*ipblock.IPBlock) (bool, error) {
cidr := subnet.CIDR()
subnetCidrIPBlock, err := ipblock.FromCidr(cidr)
if err != nil {
return false, err
}
for _, ruleIPBlock := range ruleIPBlocks {
if ruleIPBlock.Overlap(subnetCidrIPBlock) && !subnetCidrIPBlock.ContainedIn(ruleIPBlock) {
return true, nil
}
}
return false, nil
}

///////////////////////////////////////////////////////////
// finding interface implementation for splitRuleSubnet
//////////////////////////////////////////////////////////

func (finding *splitRuleSubnet) vpc() string {
return finding.vpcName
}

func (finding *splitRuleSubnet) string() string {
rule := finding.rule
thisLayerName := "Network acl"
if finding.rule.LayerName == vpcmodel.SecurityGroupLayer {
thisLayerName = "Security group"
}
subnetsStrSlice := make([]string, len(finding.splitSubnets))
for i, subnet := range finding.splitSubnets {
subnetsStrSlice[i] = fmt.Sprintf("%s (%s)", subnet.Name(), subnet.CIDR())
}
subnetStr := strings.Join(subnetsStrSlice, ", ")
if len(subnetsStrSlice) > 1 {
subnetStr = "subnets " + subnetStr
} else {
subnetStr = "subnet " + subnetStr
}
return fmt.Sprintf("In VPC %s, %s %s rule's indexed %d splits %s. Splitting rule details: %s",
finding.vpc(), thisLayerName, rule.FilterName, rule.RuleIndx, subnetStr, rule.RuleDesc)
}

// for json: a rule with the list of subnets it splits
type splitRuleSubnetJSON struct {
VpcName string `json:"vpc_name"`
Rule vpcmodel.RuleOfFilter `json:"rule_details"`
SplitSubnets []subnetJSON `json:"splitted_subnets"`
}

type subnetJSON struct {
Name string `json:"name"`
CIDR string `json:"cidr"`
}

func (finding *splitRuleSubnet) toJSON() any {
rule := finding.rule
splitSubnetsJSON := make([]subnetJSON, len(finding.splitSubnets))
for i, splitSubnet := range finding.splitSubnets {
splitSubnetsJSON[i] = subnetJSON{Name: splitSubnet.Name(), CIDR: splitSubnet.CIDR()}
}
res := splitRuleSubnetJSON{VpcName: finding.vpcName, Rule: vpcmodel.RuleOfFilter{LayerName: rule.LayerName,
FilterName: rule.FilterName, RuleIndx: rule.RuleIndx, RuleDesc: rule.RuleDesc},
SplitSubnets: splitSubnetsJSON}
return res
}
Loading