Skip to content
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

textual output with exposure analysis #331

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,13 @@ Flags:
-f, --file string Write output to specified file
--focusworkload Focus connections of specified workload in the output (supported formats: <workload-name>, <workload-namespace>/<workload-name>)
(to focus connections from Ingress/Route only, use `ingress-controller` as <workload-name>)
-o, --output string Required output format (txt, json, dot, csv, md) (default "txt")
-o, --output string Required output format (txt, json, dot, csv, md) or (txt, dot) with exposure analysis (default "txt")
-h, --help help for list

Global Flags:
-c, --context string Kubernetes context to use when evaluating connections in a live cluster
--dirpath string Resources dir path when evaluating connections from a dir
--exposure Runs also exposure analysis
--fail fail on the first encountered error
--include-json consider JSON manifests (in addition to YAML) when analyzing from dir
-k, --kubeconfig string Path and file to use for kubeconfig when evaluating connections in a live cluster
Expand Down
21 changes: 15 additions & 6 deletions pkg/cli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func buildAndExecuteCommand(args []string) (string, error) {
}

// append the optional args of a command if the values are not empty
func addCmdOptionalArgs(format, outputFile, focusWorkload string) []string {
func addCmdOptionalArgs(format, outputFile, focusWorkload string, exposure bool) []string {
res := []string{}
if focusWorkload != "" {
res = append(res, "--focusworkload", focusWorkload)
Expand All @@ -62,6 +62,9 @@ func addCmdOptionalArgs(format, outputFile, focusWorkload string) []string {
if outputFile != "" {
res = append(res, "-f", outputFile)
}
if exposure {
res = append(res, "--exposure")
}
return res
}

Expand All @@ -75,9 +78,10 @@ func determineFileSuffix(format string) string {
}

// gets the test name and name of expected output file for a list command from its args
func getListCmdTestNameAndExpectedOutputFile(dirName, focusWorkload, format string) (testName, expectedOutputFileName string) {
func getListCmdTestNameAndExpectedOutputFile(dirName, focusWorkload, format string, exposureFlag bool) (testName,
expectedOutputFileName string) {
fileSuffix := determineFileSuffix(format)
return testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, fileSuffix)
return testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, fileSuffix, exposureFlag)
}

func testInfo(testName string) string {
Expand Down Expand Up @@ -211,6 +215,7 @@ func TestListCommandOutput(t *testing.T) {
focusWorkload string
format string
outputFile string
exposureFlag bool
}{
// when focusWorkload is empty, output should be the connlist of the dir
// when format is empty - output should be in defaultFormat (txt)
Expand Down Expand Up @@ -259,13 +264,17 @@ func TestListCommandOutput(t *testing.T) {
dirName: "onlineboutique",
outputFile: outFileName,
},
{
dirName: "acs-security-demos",
exposureFlag: true,
},
}
for _, tt := range cases {
tt := tt
testName, expectedOutputFileName := getListCmdTestNameAndExpectedOutputFile(tt.dirName, tt.focusWorkload, tt.format)
testName, expectedOutputFileName := getListCmdTestNameAndExpectedOutputFile(tt.dirName, tt.focusWorkload, tt.format, tt.exposureFlag)
t.Run(testName, func(t *testing.T) {
args := []string{"list", "--dirpath", testutils.GetTestDirPath(tt.dirName)}
args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, tt.focusWorkload)...)
args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, tt.focusWorkload, tt.exposureFlag)...)
actualOut, err := buildAndExecuteCommand(args)
require.Nil(t, err, "test: %q", testName)
testutils.CheckActualVsExpectedOutputMatch(t, expectedOutputFileName, actualOut, testInfo(testName), currentPkg)
Expand Down Expand Up @@ -318,7 +327,7 @@ func TestDiffCommandOutput(t *testing.T) {
t.Run(testName, func(t *testing.T) {
args := []string{"diff", "--dir1", testutils.GetTestDirPath(tt.dir1), "--dir2",
testutils.GetTestDirPath(tt.dir2)}
args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, "")...)
args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, "", false)...)
actualOut, err := buildAndExecuteCommand(args)
require.Nil(t, err, "test: %q", testName)
testutils.CheckActualVsExpectedOutputMatch(t, expectedOutputFileName, actualOut, testInfo(testName), currentPkg)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func newCommandDiff() *cobra.Command {
c.Flags().StringVarP(&dir1, dir1Arg, "", "", "Original Resources path to be compared")
c.Flags().StringVarP(&dir2, dir2Arg, "", "", "New Resources path to compare with original resources path")
supportedDiffFormats := strings.Join(diff.ValidDiffFormats, ",")
c.Flags().StringVarP(&outFormat, "output", "o", outconsts.DefaultFormat, getOutputFormatDescription(supportedDiffFormats))
c.Flags().StringVarP(&outFormat, "output", "o", outconsts.DefaultFormat, getRequiredOutputFormatString(supportedDiffFormats))
// out file
c.Flags().StringVarP(&outFile, "file", "f", "", "Write output to specified file")
return c
Expand Down
17 changes: 13 additions & 4 deletions pkg/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,20 @@ var (
outFile string // output file
)

func getOutputFormatDescription(validFormats string) string {
// getRequiredOutputFormatString returns the description of required format(s) of the command
func getRequiredOutputFormatString(validFormats string) string {
return fmt.Sprintf("Required output format (%s)", validFormats)
}

// getListOutputFormatDescription returns the description of the required formats of the list command
// exposure analysis is supported with less formats
func getListOutputFormatDescription() string {
adisos marked this conversation as resolved.
Show resolved Hide resolved
comma := ","
supportedFormats := strings.Join(connlist.ValidFormats, comma)
supportedExposureFormats := strings.Join(connlist.ExposureValidFormats, comma)
return getRequiredOutputFormatString(supportedFormats) + " or (" + supportedExposureFormats + " with exposure analysis) "
}

func runListCommand() error {
var conns []connlist.Peer2PeerConnection
var err error
Expand Down Expand Up @@ -108,7 +118,7 @@ defined`,
k8snetpolicy list -k ./kube/config`,

PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := connlist.ValidateOutputFormat(output); err != nil {
if err := connlist.ValidateOutputFormat(output, exposureAnalysis); err != nil {
return err
}
// call parent pre-run
Expand Down Expand Up @@ -137,8 +147,7 @@ defined`,
"Focus connections of specified workload in the output (<workload-name> or <workload-namespace/workload-name>)")
c.Flags().BoolVarP(&exposureAnalysis, "exposure", "", false, "Turn on exposure analysis and append results to the output")
// output format - default txt
supportedFormats := strings.Join(connlist.ValidFormats, ",")
c.Flags().StringVarP(&output, "output", "o", outconsts.DefaultFormat, getOutputFormatDescription(supportedFormats))
c.Flags().StringVarP(&output, "output", "o", outconsts.DefaultFormat, getListOutputFormatDescription())
// out file
c.Flags().StringVarP(&outFile, "file", "f", "", "Write output to specified file")

Expand Down
6 changes: 5 additions & 1 deletion pkg/internal/testutils/testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ func GetTestDirPath(dirName string) string {
}

// ConnlistTestNameByTestArgs returns connlist test name and test's expected output file from some tests args
func ConnlistTestNameByTestArgs(dirName, focusWorkload, format string) (testName, expectedOutputFileName string) {
func ConnlistTestNameByTestArgs(dirName, focusWorkload, format string, exposureFlag bool) (testName, expectedOutputFileName string) {
namePrefix := dirName
if exposureFlag {
// if dir name contains a separator; last element is enough for the test and file names
namePrefix = "exposure_" + filepath.Base(dirName)
}
if focusWorkload != "" {
namePrefix += focusWlAnnotation + strings.Replace(focusWorkload, "/", underscore, 1)
}
Expand Down
32 changes: 19 additions & 13 deletions pkg/netpol/connlist/connlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ func (ca *ConnlistAnalyzer) ConnlistFromDirPath(dirPath string) ([]Peer2PeerConn
var ValidFormats = []string{output.TextFormat, output.JSONFormat, output.DOTFormat,
output.CSVFormat, output.MDFormat}

var ExposureValidFormats = []string{output.TextFormat}

// ConnlistAnalyzerOption is the type for specifying options for ConnlistAnalyzer,
// using Golang's Options Pattern (https://golang.cafe/blog/golang-functional-options-pattern.html).
type ConnlistAnalyzerOption func(*ConnlistAnalyzer)
Expand Down Expand Up @@ -262,12 +264,12 @@ func (ca *ConnlistAnalyzer) ConnlistFromK8sCluster(clientset *kubernetes.Clients

// ConnectionsListToString returns a string of connections from list of Peer2PeerConnection objects in the required output format
func (ca *ConnlistAnalyzer) ConnectionsListToString(conns []Peer2PeerConnection) (string, error) {
connsFormatter, err := getFormatter(ca.outputFormat)
connsFormatter, err := ca.getFormatter()
if err != nil {
ca.errors = append(ca.errors, newResultFormattingError(err))
return "", err
}
out, err := connsFormatter.writeOutput(conns)
out, err := connsFormatter.writeOutput(conns, ca.exposureResult)
if err != nil {
ca.errors = append(ca.errors, newResultFormattingError(err))
return "", err
Expand All @@ -276,8 +278,12 @@ func (ca *ConnlistAnalyzer) ConnectionsListToString(conns []Peer2PeerConnection)
}

// validate the value of the output format
func ValidateOutputFormat(format string) error {
for _, formatName := range ValidFormats {
func ValidateOutputFormat(format string, exposureFlag bool) error {
formatList := ValidFormats
if exposureFlag {
formatList = ExposureValidFormats
}
for _, formatName := range formatList {
if format == formatName {
return nil
}
Expand All @@ -286,23 +292,23 @@ func ValidateOutputFormat(format string) error {
}

// returns the relevant formatter for the analyzer's outputFormat
func getFormatter(format string) (connsFormatter, error) {
if err := ValidateOutputFormat(format); err != nil {
func (ca *ConnlistAnalyzer) getFormatter() (connsFormatter, error) {
if err := ValidateOutputFormat(ca.outputFormat, ca.exposureAnalysis); err != nil {
return nil, err
}
switch format {
switch ca.outputFormat {
case output.JSONFormat:
return formatJSON{}, nil
return &formatJSON{}, nil
case output.TextFormat:
return formatText{}, nil
return &formatText{}, nil
case output.DOTFormat:
return formatDOT{}, nil
return &formatDOT{}, nil
case output.CSVFormat:
return formatCSV{}, nil
return &formatCSV{}, nil
case output.MDFormat:
return formatMD{}, nil
return &formatMD{}, nil
default:
return formatText{}, nil
return &formatText{}, nil
}
}

Expand Down
105 changes: 94 additions & 11 deletions pkg/netpol/connlist/connlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestConnListFromDir(t *testing.T) {
t.Run(tt.testDirName, func(t *testing.T) {
t.Parallel()
for _, format := range tt.outputFormats {
pTest := prepareTest(tt.testDirName, tt.focusWorkload, format)
pTest := prepareTest(tt.testDirName, tt.focusWorkload, format, tt.exposureAnalysis)
res, _, err := pTest.analyzer.ConnlistFromDirPath(pTest.dirPath)
require.Nil(t, err, pTest.testInfo)
out, err := pTest.analyzer.ConnectionsListToString(res)
Expand All @@ -95,7 +95,7 @@ func TestConnListFromResourceInfos(t *testing.T) {
t.Run(tt.testDirName, func(t *testing.T) {
t.Parallel()
for _, format := range tt.outputFormats {
pTest := prepareTest(tt.testDirName, tt.focusWorkload, format)
pTest := prepareTest(tt.testDirName, tt.focusWorkload, format, tt.exposureAnalysis)
infos, _ := fsscanner.GetResourceInfosFromDirPath([]string{pTest.dirPath}, true, false)
// require.Empty(t, errs, testInfo) - TODO: add info about expected errors
// from each test here (these errors do not stop the analysis or affect the output)
Expand Down Expand Up @@ -189,7 +189,7 @@ func testFatalErr(t *testing.T,

func getAnalysisResFromAPI(apiName, dirName, focusWorkload string) (
analyzer *ConnlistAnalyzer, connsRes []Peer2PeerConnection, peersRes []Peer, err error) {
pTest := prepareTest(dirName, focusWorkload, output.DefaultFormat)
pTest := prepareTest(dirName, focusWorkload, output.DefaultFormat, false)
switch apiName {
case ResourceInfosFunc:
infos, _ := fsscanner.GetResourceInfosFromDirPath([]string{pTest.dirPath}, true, false)
Expand Down Expand Up @@ -468,11 +468,15 @@ type preparedTest struct {
analyzer *ConnlistAnalyzer
}

func prepareTest(dirName, focusWorkload, format string) preparedTest {
func prepareTest(dirName, focusWorkload, format string, exposureFlag bool) preparedTest {
res := preparedTest{}
res.testName, res.expectedOutputFileName = testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, format)
res.testName, res.expectedOutputFileName = testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, format, exposureFlag)
res.testInfo = fmt.Sprintf("test: %q, output format: %q", res.testName, format)
res.analyzer = NewConnlistAnalyzer(WithOutputFormat(format), WithFocusWorkload(focusWorkload))
cAnalyzer := NewConnlistAnalyzer(WithOutputFormat(format), WithFocusWorkload(focusWorkload))
if exposureFlag {
cAnalyzer = NewConnlistAnalyzer(WithOutputFormat(format), WithFocusWorkload(focusWorkload), WithExposureAnalysis())
}
res.analyzer = cAnalyzer
res.dirPath = testutils.GetTestDirPath(dirName)
return res
}
Expand All @@ -488,19 +492,27 @@ func TestConnlistOutputFatalErrors(t *testing.T) {
dirName string
format string
errorStrContains string
exposureFlag bool
}{
{
name: "giving_unsupported_output_format_option_should_return_fatal_error",
dirName: "onlineboutique",
format: "docx",
errorStrContains: netpolerrors.FormatNotSupportedErrStr("docx"),
},
{
name: "unsupported_output_format_for_exposure_analysis_should_return_fatal_error",
dirName: "acs-security-demos",
format: "json",
errorStrContains: netpolerrors.FormatNotSupportedErrStr("json"),
exposureFlag: true,
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
preparedTest := prepareTest(tt.dirName, "", tt.format)
preparedTest := prepareTest(tt.dirName, "", tt.format, tt.exposureFlag)
connsRes, peersRes, err := preparedTest.analyzer.ConnlistFromDirPath(preparedTest.dirPath)

require.Nil(t, err, tt.name)
Expand All @@ -514,7 +526,7 @@ func TestConnlistOutputFatalErrors(t *testing.T) {
testutils.CheckErrorContainment(t, tt.name, tt.errorStrContains, err.Error())

// re-run the test with new analyzer (to clear the analyzer.errors array )
preparedTest = prepareTest(tt.dirName, "", tt.format)
preparedTest = prepareTest(tt.dirName, "", tt.format, tt.exposureFlag)
infos, _ := fsscanner.GetResourceInfosFromDirPath([]string{preparedTest.dirPath}, true, false)
connsRes2, peersRes2, err2 := preparedTest.analyzer.ConnlistFromResourceInfos(infos)

Expand All @@ -531,9 +543,10 @@ func TestConnlistOutputFatalErrors(t *testing.T) {
}

var goodPathTests = []struct {
testDirName string
outputFormats []string
focusWorkload string
testDirName string
outputFormats []string
focusWorkload string
exposureAnalysis bool
}{
{
testDirName: "ipblockstest",
Expand Down Expand Up @@ -737,4 +750,74 @@ var goodPathTests = []struct {
focusWorkload: "ingress-controller",
outputFormats: []string{output.TextFormat},
},
{
testDirName: "acs-security-demos",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_allow_all",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_allow_all_in_cluster",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_allow_egress_deny_ingress",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_allow_ingress_deny_egress",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_matched_and_unmatched_rules",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_only_matched_rules",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_multiple_unmatched_rules",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_new_namespace_conn_and_entire_cluster",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_same_unmatched_rule_in_ingress_egress",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_with_no_netpols",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_egress_to_entire_cluster_with_named_ports",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_ingress_from_entire_cluster_with_named_ports",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_egress_exposure_with_named_port",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
},
}
Loading