diff --git a/cmd/kosli/createAttestationType.go b/cmd/kosli/createAttestationType.go index 897aa991e..e91357cb1 100644 --- a/cmd/kosli/createAttestationType.go +++ b/cmd/kosli/createAttestationType.go @@ -1,9 +1,13 @@ package main import ( + "encoding/json" + "errors" + "fmt" "io" "net/http" "net/url" + "strings" "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" @@ -26,6 +30,18 @@ See an example schema file These rules specify acceptable values for attestation data, e.g. ^.age >= 21^ or ^.failing_tests == 0^. When a custom attestation is reported, the provided data is evaluated according to the rules defined in its attestation-type. All rules must return ^true^ for the evaluation to pass and the attestation to be determined compliant. + +^--summary^ defines one entry of the summary shown for attestations of this type, given as +^'NAME=EXPRESSION'^ where the expression is a jq expression evaluated against the attestation data. +The flag can be repeated to add further entries, which are displayed in the order given, e.g. +^--summary "Critical=.critical_count" --summary "Tool=.scanner.name"^. +Each value is split on its first ^=^ only, so jq expressions containing ^==^ are unaffected. + +^--summary-json^ is an alternative to ^--summary^ for summaries that are easier to express as JSON, +given as a JSON array of ^{"name": ..., "expression": ...}^ entries, e.g. +^'[{"name":"Critical","expression":".critical_count"}]'^. The two summary flags cannot be combined. + +Attestation types created without a summary fall back to the jq evaluation rules checklist. ` const createAttestationTypeExample = ` @@ -38,12 +54,32 @@ kosli create attestation-type customTypeName \ --schema person-schema.json \ --jq ".age >= 18" --jq ".age < 65" + +# create/update a custom attestation type with a summary: +kosli create attestation-type customTypeName \ + --schema scan-schema.json \ + --summary "Critical=.critical_count" \ + --summary "Tool=.scanner.name" + +# create/update a custom attestation type with a summary given as JSON: +kosli create attestation-type customTypeName \ + --schema scan-schema.json \ + --summary-json '[{"name":"Critical","expression":".critical_count"},{"name":"Tool","expression":".scanner.name"}]' ` type createAttestationTypeOptions struct { - payload CreateAttestationTypePayload - schemaFilePath string - jqRules []string + payload CreateAttestationTypePayload + schemaFilePath string + jqRules []string + summaryJSON string + summaryKeyValue []string +} + +// SummaryEntry is one named jq expression displayed in the summary of +// attestations made using a custom attestation type. +type SummaryEntry struct { + Name string `json:"name"` + Expression string `json:"expression"` } type JQEvaluatorPayload struct { @@ -59,6 +95,73 @@ type CreateAttestationTypePayload struct { TypeName string `json:"name"` Description string `json:"description,omitempty"` Evaluator *JQEvaluatorPayload `json:"evaluator,omitempty"` + Summary []SummaryEntry `json:"summary,omitempty"` +} + +// parseSummaryJSON parses the --summary-json flag value into an ordered list of +// summary entries. The value must be a JSON array of {name, expression} objects, +// both fields non-empty. +func parseSummaryJSON(value string) ([]SummaryEntry, error) { + if strings.TrimSpace(value) == "" { + return nil, nil + } + + var summary []SummaryEntry + if err := json.Unmarshal([]byte(value), &summary); err != nil { + var syntaxErr *json.SyntaxError + if errors.As(err, &syntaxErr) { + return nil, fmt.Errorf("--summary-json is not valid JSON: %s", err.Error()) + } + return nil, fmt.Errorf("--summary-json must be a JSON array of {name, expression} entries") + } + + for i, entry := range summary { + if strings.TrimSpace(entry.Name) == "" { + return nil, fmt.Errorf("--summary-json entry %d is missing a name", i+1) + } + if strings.TrimSpace(entry.Expression) == "" { + return nil, fmt.Errorf("--summary-json entry %d is missing an expression", i+1) + } + } + + return summary, nil +} + +// parseSummaryFlags parses repeated --summary NAME=EXPRESSION values into an +// ordered list of summary entries, preserving the order the flags were given in. +// Each value is split on its first "=" only, so JQ expressions containing "==" +// or assignments keep their meaning; whitespace around both halves is trimmed. +func parseSummaryFlags(values []string) ([]SummaryEntry, error) { + if len(values) == 0 { + return nil, nil + } + + summary := make([]SummaryEntry, 0, len(values)) + for i, value := range values { + name, expression, found := strings.Cut(value, "=") + if !found { + return nil, fmt.Errorf("--summary entry %d must be in the form NAME=EXPRESSION", i+1) + } + + name = strings.TrimSpace(name) + expression = strings.TrimSpace(expression) + if name == "" { + return nil, fmt.Errorf("--summary entry %d is missing a name", i+1) + } + if expression == "" { + return nil, fmt.Errorf("--summary entry %d is missing an expression", i+1) + } + // Catches a bare jq expression passed without a name: ".failing == 0" + // splits to {".failing", "= 0"}. No valid jq expression starts with "=", + // so a leading one always means the value was not NAME=EXPRESSION. + if strings.HasPrefix(expression, "=") { + return nil, fmt.Errorf("--summary entry %d expression cannot start with '='", i+1) + } + + summary = append(summary, SummaryEntry{Name: name, Expression: expression}) + } + + return summary, nil } func newCreateAttestationTypeCmd(out io.Writer) *cobra.Command { @@ -74,7 +177,10 @@ func newCreateAttestationTypeCmd(out io.Writer) *cobra.Command { if err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } - return nil + + // Both flags set the same payload field, so letting one silently win + // would be a trap. + return MuXRequiredFlags(cmd, []string{"summary", "summary-json"}, false) }, RunE: func(cmd *cobra.Command, args []string) error { return o.run(args) @@ -84,6 +190,8 @@ func newCreateAttestationTypeCmd(out io.Writer) *cobra.Command { cmd.Flags().StringVarP(&o.payload.Description, "description", "d", "", attestationTypeDescriptionFlag) cmd.Flags().StringVarP(&o.schemaFilePath, "schema", "s", "", attestationTypeSchemaFlag) cmd.Flags().StringArrayVar(&o.jqRules, "jq", []string{}, attestationTypeJqFlag) + cmd.Flags().StringArrayVar(&o.summaryKeyValue, "summary", []string{}, attestationTypeSummaryFlag) + cmd.Flags().StringVar(&o.summaryJSON, "summary-json", "", attestationTypeSummaryJsonFlag) addDryRunFlag(cmd) return cmd @@ -95,6 +203,20 @@ func (o *createAttestationTypeOptions) run(args []string) error { o.payload.Evaluator = NewJQEvaluatorPayload(o.jqRules) } + summary, err := parseSummaryJSON(o.summaryJSON) + if err != nil { + return err + } + if summary == nil { + // --summary and --summary-json are mutually exclusive, so at most one of + // these two parses can yield entries. + summary, err = parseSummaryFlags(o.summaryKeyValue) + if err != nil { + return err + } + } + o.payload.Summary = summary + form, err := prepareAttestationTypeForm(o.payload, o.schemaFilePath) if err != nil { return err diff --git a/cmd/kosli/createAttestationType_test.go b/cmd/kosli/createAttestationType_test.go index 3f962372b..35f95f498 100644 --- a/cmd/kosli/createAttestationType_test.go +++ b/cmd/kosli/createAttestationType_test.go @@ -1,9 +1,11 @@ package main import ( + "encoding/json" "fmt" "testing" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -57,6 +59,128 @@ func (suite *CreateAttestationTypeTestSuite) TestCustomAttestationTypeCmd() { cmd: `create attestation-type wibble-6 --jq '.name | startswith("B")'` + suite.defaultKosliArguments, golden: "attestation-type wibble-6 was created\n", }, + { + name: "summary json is provided", + cmd: `create attestation-type wibble-7 --summary-json '[{"name":"Critical","expression":".critical_count"}]'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-7 was created\n", + }, + { + name: "summary json expressions can contain commas and equals", + cmd: `create attestation-type wibble-8 --summary-json '[{"name":"Tool","expression":"[.a, .b] | map(select(.x == 1)) | length"}]'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-8 was created\n", + }, + { + name: "empty summary json array is accepted", + cmd: `create attestation-type wibble-9 --summary-json '[]'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-9 was created\n", + }, + { + // Deliberately asymmetric with the empty --summary case below: a blank + // JSON blob reads as "not given", a blank key=value entry as malformed. + name: "empty summary json string is accepted", + cmd: `create attestation-type wibble-10 --summary-json ''` + suite.defaultKosliArguments, + golden: "attestation-type wibble-10 was created\n", + }, + { + name: "summary json and jq rules can be combined", + cmd: `create attestation-type wibble-11 --jq '.critical_count == 0' --summary-json '[{"name":"Critical","expression":".critical_count"}]'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-11 was created\n", + }, + { + name: "summary json and schema can be combined", + cmd: `create attestation-type wibble-12 --schema testdata/person-schema.json --summary-json '[{"name":"Age","expression":".age"}]'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-12 was created\n", + }, + { + wantError: true, + name: "fails when summary json is malformed", + cmd: `create attestation-type wibble-bad --summary-json '[{"name":'` + suite.defaultKosliArguments, + golden: "Error: --summary-json is not valid JSON: unexpected end of JSON input\n", + }, + { + wantError: true, + name: "fails when summary json is an object not an array", + cmd: `create attestation-type wibble-bad --summary-json '{"name":"Critical","expression":".critical_count"}'` + suite.defaultKosliArguments, + golden: "Error: --summary-json must be a JSON array of {name, expression} entries\n", + }, + { + wantError: true, + name: "fails when a summary entry has no name", + cmd: `create attestation-type wibble-bad --summary-json '[{"expression":".critical_count"}]'` + suite.defaultKosliArguments, + golden: "Error: --summary-json entry 1 is missing a name\n", + }, + { + wantError: true, + name: "fails when a summary entry has no expression", + cmd: `create attestation-type wibble-bad --summary-json '[{"name":"Critical"},{"name":"Tool","expression":".t"}]'` + suite.defaultKosliArguments, + golden: "Error: --summary-json entry 1 is missing an expression\n", + }, + { + name: "repeatable summary is provided", + cmd: `create attestation-type wibble-13 --summary 'Critical=.critical_count' --summary 'Tool=.scanner.name'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-13 was created\n", + }, + { + name: "repeatable summary expressions can contain commas and equals", + cmd: `create attestation-type wibble-14 --summary 'Tool=[.a, .b] | map(select(.x == 1)) | length'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-14 was created\n", + }, + { + name: "repeatable summary and jq rules can be combined", + cmd: `create attestation-type wibble-15 --jq '.critical_count == 0' --summary 'Critical=.critical_count'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-15 was created\n", + }, + { + name: "repeatable summary and schema can be combined", + cmd: `create attestation-type wibble-16 --schema testdata/person-schema.json --summary 'Age=.age'` + suite.defaultKosliArguments, + golden: "attestation-type wibble-16 was created\n", + }, + { + wantError: true, + name: "fails when a repeatable summary entry has no equals sign", + cmd: `create attestation-type wibble-bad --summary 'Critical'` + suite.defaultKosliArguments, + golden: "Error: --summary entry 1 must be in the form NAME=EXPRESSION\n", + }, + { + wantError: true, + name: "fails when a repeatable summary entry has no name", + cmd: `create attestation-type wibble-bad --summary '=.critical_count'` + suite.defaultKosliArguments, + golden: "Error: --summary entry 1 is missing a name\n", + }, + { + wantError: true, + name: "fails when a repeatable summary entry has no expression", + cmd: `create attestation-type wibble-bad --summary 'Critical='` + suite.defaultKosliArguments, + golden: "Error: --summary entry 1 is missing an expression\n", + }, + { + wantError: true, + name: "fails when a repeatable summary value is a bare expression", + cmd: `create attestation-type wibble-bad --summary '.failing == 0'` + suite.defaultKosliArguments, + golden: "Error: --summary entry 1 expression cannot start with '='\n", + }, + { + // Unlike --summary-json '', which is a no-op. Matters in CI, where + // --summary "$VAR" with VAR unset fails instead of silently no-opping. + wantError: true, + name: "fails when a repeatable summary value is empty", + cmd: `create attestation-type wibble-bad --summary ''` + suite.defaultKosliArguments, + golden: "Error: --summary entry 1 must be in the form NAME=EXPRESSION\n", + }, + { + wantError: true, + name: "fails when both summary flags are provided", + cmd: `create attestation-type wibble-bad --summary 'Critical=.critical_count' --summary-json '[{"name":"Critical","expression":".critical_count"}]'` + suite.defaultKosliArguments, + golden: "Error: only one of --summary, --summary-json is allowed\n", + }, + { + // MuXRequiredFlags keys off flag.Changed, so an explicitly empty + // --summary-json still trips the exclusion despite being a no-op. + wantError: true, + name: "fails when both summary flags are provided and summary json is empty", + cmd: `create attestation-type wibble-bad --summary 'Critical=.critical_count' --summary-json ''` + suite.defaultKosliArguments, + golden: "Error: only one of --summary, --summary-json is allowed\n", + }, } runTestCmd(suite.T(), tests) @@ -67,3 +191,200 @@ func (suite *CreateAttestationTypeTestSuite) TestCustomAttestationTypeCmd() { func TestCreateAttestationTypeTestSuite(t *testing.T) { suite.Run(t, new(CreateAttestationTypeTestSuite)) } + +func TestParseSummaryJSON(t *testing.T) { + t.Run("omitted flag leaves summary out of the payload", func(t *testing.T) { + payload := CreateAttestationTypePayload{TypeName: "wibble"} + body, err := json.Marshal(payload) + require.NoError(t, err) + require.NotContains(t, string(body), "summary") + }) + + t.Run("empty array leaves summary out of the payload", func(t *testing.T) { + summary, err := parseSummaryJSON("[]") + require.NoError(t, err) + require.Empty(t, summary) + + payload := CreateAttestationTypePayload{TypeName: "wibble", Summary: summary} + body, err := json.Marshal(payload) + require.NoError(t, err) + require.NotContains(t, string(body), "summary") + }) + + t.Run("entries keep their order and round-trip unmangled", func(t *testing.T) { + summary, err := parseSummaryJSON(`[{"name":"Critical","expression":"[.a, .b] | map(select(.x == 1)) | length"},{"name":"Tool","expression":".scanner.name"}]`) + require.NoError(t, err) + require.Equal(t, []SummaryEntry{ + {Name: "Critical", Expression: "[.a, .b] | map(select(.x == 1)) | length"}, + {Name: "Tool", Expression: ".scanner.name"}, + }, summary) + }) + + // An unset flag arrives here as "". Parsing it must stay a no-op rather than + // falling through to json.Unmarshal, which rejects an empty string. + t.Run("blank values are treated as no summary", func(t *testing.T) { + for _, value := range []string{"", " ", "\t\n"} { + summary, err := parseSummaryJSON(value) + require.NoErrorf(t, err, "value %q", value) + require.Emptyf(t, summary, "value %q", value) + } + }) + + t.Run("rejects entries that are not usable", func(t *testing.T) { + for _, tc := range []struct { + name string + value string + wantErr string + }{ + { + name: "whitespace-only name", + value: `[{"name":" ","expression":".x"}]`, + wantErr: "--summary-json entry 1 is missing a name", + }, + { + name: "whitespace-only expression", + value: `[{"name":"Critical","expression":" "}]`, + wantErr: "--summary-json entry 1 is missing an expression", + }, + { + // Proves the reported index tracks the entry's position rather + // than being coincidentally right for a single-entry list. + name: "second entry is the bad one", + value: `[{"name":"A","expression":".a"},{"name":"B"}]`, + wantErr: "--summary-json entry 2 is missing an expression", + }, + { + name: "null entry", + value: `[null]`, + wantErr: "--summary-json entry 1 is missing a name", + }, + { + name: "array of numbers", + value: `[1,2,3]`, + wantErr: "--summary-json must be a JSON array of {name, expression} entries", + }, + { + name: "array of strings", + value: `["a"]`, + wantErr: "--summary-json must be a JSON array of {name, expression} entries", + }, + { + name: "trailing data after the array", + value: `[] trailing`, + wantErr: "--summary-json is not valid JSON: invalid character 't' after top-level value", + }, + } { + t.Run(tc.name, func(t *testing.T) { + summary, err := parseSummaryJSON(tc.value) + require.EqualError(t, err, tc.wantErr) + require.Empty(t, summary) + }) + } + }) +} + +func TestParseSummaryFlags(t *testing.T) { + t.Run("no flags leaves summary out of the payload", func(t *testing.T) { + summary, err := parseSummaryFlags(nil) + require.NoError(t, err) + require.Empty(t, summary) + + payload := CreateAttestationTypePayload{TypeName: "wibble", Summary: summary} + body, err := json.Marshal(payload) + require.NoError(t, err) + require.NotContains(t, string(body), "summary") + }) + + t.Run("entries keep the order the flags were given in", func(t *testing.T) { + summary, err := parseSummaryFlags([]string{"Critical=.critical_count", "Tool=.scanner.name"}) + require.NoError(t, err) + require.Equal(t, []SummaryEntry{ + {Name: "Critical", Expression: ".critical_count"}, + {Name: "Tool", Expression: ".scanner.name"}, + }, summary) + }) + + // The whole reason for splitting on the first "=" only: JQ expressions use + // "==" for comparison, and commas are meaningful inside them. Both must + // survive into the expression untouched. + t.Run("splits on the first equals only", func(t *testing.T) { + summary, err := parseSummaryFlags([]string{"Tool=[.a, .b] | map(select(.x == 1)) | length"}) + require.NoError(t, err) + require.Equal(t, []SummaryEntry{ + {Name: "Tool", Expression: "[.a, .b] | map(select(.x == 1)) | length"}, + }, summary) + }) + + t.Run("whitespace around name and expression is trimmed", func(t *testing.T) { + summary, err := parseSummaryFlags([]string{" Critical = .critical_count "}) + require.NoError(t, err) + require.Equal(t, []SummaryEntry{ + {Name: "Critical", Expression: ".critical_count"}, + }, summary) + }) + + t.Run("rejects entries that are not usable", func(t *testing.T) { + for _, tc := range []struct { + name string + values []string + wantErr string + }{ + { + name: "no separator", + values: []string{"Critical"}, + wantErr: "--summary entry 1 must be in the form NAME=EXPRESSION", + }, + { + name: "empty value", + values: []string{""}, + wantErr: "--summary entry 1 must be in the form NAME=EXPRESSION", + }, + { + name: "empty name", + values: []string{"=.critical_count"}, + wantErr: "--summary entry 1 is missing a name", + }, + { + name: "whitespace-only name", + values: []string{" =.critical_count"}, + wantErr: "--summary entry 1 is missing a name", + }, + { + name: "empty expression", + values: []string{"Critical="}, + wantErr: "--summary entry 1 is missing an expression", + }, + { + name: "whitespace-only expression", + values: []string{"Critical= "}, + wantErr: "--summary entry 1 is missing an expression", + }, + { + // A bare jq expression using "==" carries a separator, so it + // would otherwise parse to {".failing", "= 0"}. No valid jq + // expression starts with "=" — it is only ever infix. + name: "bare expression using ==", + values: []string{".failing == 0"}, + wantErr: "--summary entry 1 expression cannot start with '='", + }, + { + name: "doubled separator", + values: []string{"Critical== 0"}, + wantErr: "--summary entry 1 expression cannot start with '='", + }, + { + // Proves the reported index tracks the flag's position rather + // than being coincidentally right for a single entry. + name: "second entry is the bad one", + values: []string{"A=.a", "B"}, + wantErr: "--summary entry 2 must be in the form NAME=EXPRESSION", + }, + } { + t.Run(tc.name, func(t *testing.T) { + summary, err := parseSummaryFlags(tc.values) + require.EqualError(t, err, tc.wantErr) + require.Empty(t, summary) + }) + } + }) +} diff --git a/cmd/kosli/getAttestationType.go b/cmd/kosli/getAttestationType.go index 93f4ff74b..fbac38279 100644 --- a/cmd/kosli/getAttestationType.go +++ b/cmd/kosli/getAttestationType.go @@ -190,5 +190,18 @@ func printVersionedAttestationTypeAsTable(raw map[string]interface{}, rows []str } } + // Types created without a summary have a null "summary", which fails this + // type assertion and prints nothing, leaving their output unchanged. + if summary, ok := attestationType["summary"].([]interface{}); ok && len(summary) > 0 { + rows = append(rows, " Summary:\t") + for _, entry := range summary { + entryMap, ok := entry.(map[string]interface{}) + if !ok { + continue + } + rows = append(rows, fmt.Sprintf(" %s:\t%s", entryMap["name"], entryMap["expression"])) + } + } + return rows, nil } diff --git a/cmd/kosli/getAttestationType_test.go b/cmd/kosli/getAttestationType_test.go index 85b434a0c..d9b5e2a21 100644 --- a/cmd/kosli/getAttestationType_test.go +++ b/cmd/kosli/getAttestationType_test.go @@ -14,12 +14,14 @@ type GetAttestationTypeCommandTestSuite struct { suite.Suite attestationTypeName string archivedTypeName string + summaryTypeName string defaultKosliArguments string } func (suite *GetAttestationTypeCommandTestSuite) SetupTest() { suite.attestationTypeName = "custom-attestation-type-1" suite.archivedTypeName = "archived-type" + suite.summaryTypeName = "summary-type" global = &GlobalOpts{ ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY", Org: "docs-cmd-test-user-shared", @@ -30,6 +32,8 @@ func (suite *GetAttestationTypeCommandTestSuite) SetupTest() { CreateCustomAttestationType(suite.attestationTypeName, "testdata/person-schema.json", []string{".age > 21"}, suite.T()) CreateCustomAttestationType(suite.archivedTypeName, "testdata/person-schema.json", []string{".age < 21"}, suite.T()) ArchiveCustomAttestationType(suite.archivedTypeName, suite.T()) + CreateCustomAttestationTypeWithSummary(suite.summaryTypeName, "testdata/person-schema.json", []string{".age > 21"}, + `[{"name":"Age","expression":".age"},{"name":"Name","expression":".name"}]`, suite.T()) } func (suite *GetAttestationTypeCommandTestSuite) TestGetAttestationTypeCmd() { @@ -73,6 +77,11 @@ func (suite *GetAttestationTypeCommandTestSuite) TestGetAttestationTypeCmd() { cmd: fmt.Sprintf(`get attestation-type %s@v1 %s`, suite.attestationTypeName, suite.defaultKosliArguments), goldenFile: "output/get/get-attestation-type-version.txt", }, + { + name: "summary entries are shown in the table output", + cmd: fmt.Sprintf(`get attestation-type %s %s`, suite.summaryTypeName, suite.defaultKosliArguments), + goldenFile: "output/get/get-attestation-type-with-summary.txt", + }, { name: "getting an existing attestation type with --output json works", cmd: fmt.Sprintf(`get attestation-type %s --output json %s`, suite.attestationTypeName, suite.defaultKosliArguments), diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index df48f7e78..ae448c96e 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -309,6 +309,8 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, attestationTypeDescriptionFlag = "[optional] The attestation type description." attestationTypeSchemaFlag = "[optional] Path to the attestation type schema in JSON Schema format." attestationTypeJqFlag = "[optional] The attestation type evaluation JQ rules." + attestationTypeSummaryFlag = "[optional] An attestation type summary entry, given as 'NAME=EXPRESSION'. Can be repeated. Cannot be used with --summary-json." + attestationTypeSummaryJsonFlag = "[optional] The attestation type summary, given as a JSON array of {name, expression} entries, e.g. '[{\"name\":\"Critical\",\"expression\":\".critical_count\"}]'. Cannot be used with --summary." controlNameFlag = "[required] The control name." updateControlNameFlag = "[optional] The new control name." controlDescriptionFlag = "[optional] The control description." diff --git a/cmd/kosli/testHelpers.go b/cmd/kosli/testHelpers.go index 73caf4c1c..8da6f50a1 100644 --- a/cmd/kosli/testHelpers.go +++ b/cmd/kosli/testHelpers.go @@ -336,6 +336,14 @@ func ArchiveCustomAttestationType(typeName string, t *testing.T) { } func CreateCustomAttestationType(typeName, schemaFilePath string, jqEvaluators []string, t *testing.T) { + t.Helper() + CreateCustomAttestationTypeWithSummary(typeName, schemaFilePath, jqEvaluators, "", t) +} + +// CreateCustomAttestationTypeWithSummary creates an attestation type whose summary +// is set from summaryJSON, in the same form the --summary-json flag takes. An empty +// summaryJSON creates a type without a summary. +func CreateCustomAttestationTypeWithSummary(typeName, schemaFilePath string, jqEvaluators []string, summaryJSON string, t *testing.T) { t.Helper() o := &createAttestationTypeOptions{ payload: CreateAttestationTypePayload{ @@ -343,6 +351,7 @@ func CreateCustomAttestationType(typeName, schemaFilePath string, jqEvaluators [ }, schemaFilePath: schemaFilePath, jqRules: jqEvaluators, + summaryJSON: summaryJSON, } err := o.run([]string{typeName}) require.NoError(t, err, "attestation type should be created without error") diff --git a/cmd/kosli/testdata/output/get/get-attestation-type-with-summary.txt b/cmd/kosli/testdata/output/get/get-attestation-type-with-summary.txt new file mode 100644 index 000000000..fdc34df76 --- /dev/null +++ b/cmd/kosli/testdata/output/get/get-attestation-type-with-summary.txt @@ -0,0 +1,19 @@ +Name: summary-type +Organization: docs-cmd-test-user-shared +Archived: false +Created By: docs-cmd-test-user +Created at: .* +Last modified at: .* +Versions: + Version: 1 + Timestamp: .* + Created By: docs-cmd-test-user + Type schema: \{"additionalProperties":true,"properties":\{"age":\{"type":"integer"\},"name":\{"type":"string"\}\},"type":"object"\} + Evaluator: + Content Type: jq + Rules: + .age > 21 + Summary: + Age: .age + Name: .name + \ No newline at end of file diff --git a/go.mod b/go.mod index e43d03c81..c8ca06b42 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kosli-dev/cli -go 1.26.5 +go 1.26.6 require ( cloud.google.com/go/run v1.22.0