Skip to content
Open
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
29 changes: 27 additions & 2 deletions cmd/complypack/cli/applicability.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,31 @@ func writeApplicabilityHuman(
catalogName string,
result *requirement.ApplicabilityGroupResult,
) error {
// TODO: add styled output with lipgloss formatting.
return writeApplicabilityText(w, catalogName, result)
fmt.Fprintln(w, renderHeader(fmt.Sprintf("Applicability: %s", catalogName)))
fmt.Fprintln(w, fmt.Sprintf(" %s %s", renderMetadata("Groups", len(result.Groups)),
renderMetadata("Ungrouped", len(result.Ungrouped))),
)

for _, g := range result.Groups {
fmt.Fprintf(w, "\n %s", styleOK.Render(g.ID))
if g.Title != "" {
fmt.Fprintf(w, " - %s", styleControl.Render(g.Title))
}
fmt.Fprintln(w)
if g.Description != "" {
fmt.Fprintf(w, " %s\n", styleDim.Render(g.Description))
}
if len(g.RequirementIDs) > 0 {
fmt.Fprintf(w, " %s: %s\n", styleOK.Render("Requirements"), styleDim.Render(strings.Join(g.RequirementIDs, ", ")))
}
}

if len(result.Ungrouped) > 0 {
fmt.Fprintln(w, "\n "+
styleWarn.Render("Ungrouped:")+" "+
strings.Join(result.Ungrouped, ", "),
)
}

return nil
}
77 changes: 65 additions & 12 deletions cmd/complypack/cli/applicability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,25 +187,78 @@ func TestWriteApplicabilityJSON(t *testing.T) {
assert.Equal(t, []string{"R3"}, parsed.Ungrouped)
}

func TestWriteApplicabilityHuman_DelegatesToText(
t *testing.T,
) {
func TestWriteApplicabilityHuman(t *testing.T) {
result := &requirement.ApplicabilityGroupResult{
Groups: []requirement.ApplicabilityGroupInfo{},
Ungrouped: []string{},
Groups: []requirement.ApplicabilityGroupInfo{
{
ID: "maturity-1",
Title: "Maturity Level 1",
Description: "Basic requirements",
RequirementIDs: []string{"REQ-001", "REQ-002"},
},
{
ID: "maturity-2",
Title: "Maturity Level 2",
RequirementIDs: []string{"REQ-003"},
},
},
Ungrouped: []string{"REQ-099"},
}

var textBuf, humanBuf bytes.Buffer
err := writeApplicabilityText(
&textBuf, "cat", result,
var buf bytes.Buffer
err := writeApplicabilityHuman(
&buf, "my-catalog", result,
)
require.NoError(t, err)

err = writeApplicabilityHuman(
&humanBuf, "cat", result,
output := buf.String()

// Verify styled header
assert.Contains(t, output, "my-catalog")
assert.Contains(t, output, "━",
"human output should contain styled header separator")

// Verify data passes through
assert.Contains(t, output, "Groups:")
assert.Contains(t, output, "maturity-1")
assert.Contains(t, output, "Maturity Level 1")
assert.Contains(t, output, "Basic requirements")
assert.Contains(t, output, "REQ-001")
assert.Contains(t, output, "REQ-002")
assert.Contains(t, output, "maturity-2")
assert.Contains(t, output, "REQ-003")
assert.Contains(t, output, "Ungrouped:")
assert.Contains(t, output, "REQ-099")

// Verify human output differs from text output
var textBuf bytes.Buffer
err = writeApplicabilityText(
&textBuf, "my-catalog", result,
)
require.NoError(t, err)

assert.NotEqual(t, textBuf.String(), buf.String(),
"human format should differ from text format")
}

func TestWriteApplicabilityHuman_NoGroups(t *testing.T) {
result := &requirement.ApplicabilityGroupResult{
Groups: []requirement.ApplicabilityGroupInfo{},
Ungrouped: []string{},
}

var buf bytes.Buffer
err := writeApplicabilityHuman(
&buf, "empty-catalog", result,
)
require.NoError(t, err)

assert.Equal(t, textBuf.String(), humanBuf.String(),
"human format should delegate to text for now")
output := buf.String()
assert.Contains(t, output, "empty-catalog")
assert.Contains(t, output, "━",
"human output should contain styled header separator")
assert.Contains(t, output, "Groups:")
assert.Contains(t, output, "Ungrouped:")
// Ungrouped section (with requirement list) should not appear
assert.NotContains(t, output, "REQ-")
}
40 changes: 2 additions & 38 deletions cmd/complypack/cli/coverage.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,6 @@ import (
"github.com/spf13/cobra"
)

// Output format constants.
const (
formatHuman = "human"
formatText = "text"
formatJSON = "json"
)

var (
styleTitle = lipgloss.NewStyle().Bold(true)
styleControl = lipgloss.NewStyle().Bold(true)
stylePass = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
styleFail = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
styleGap = lipgloss.NewStyle().Foreground(lipgloss.Color("214"))
styleOK = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
styleWarn = lipgloss.NewStyle().Foreground(lipgloss.Color("214"))
styleDim = lipgloss.NewStyle().Faint(true)
)

func coverageCmd() *cobra.Command {
var (
policyName string
Expand Down Expand Up @@ -108,23 +90,6 @@ Examples:
return cmd
}

// resolveFormat determines the output format from the flag value and environment.
// When no flag is provided, it defaults to "text" if NO_COLOR is set, otherwise "human".
func resolveFormat(flagValue string) (string, error) {
if flagValue != "" {
switch flagValue {
case formatHuman, formatText, formatJSON:
return flagValue, nil
default:
return "", fmt.Errorf("unknown format %q; valid formats: human, text, json", flagValue)
}
}
if os.Getenv("NO_COLOR") != "" {
return formatText, nil
}
return formatHuman, nil
}

// coverageRunParams holds parsed CLI parameters for the coverage command.
type coverageRunParams struct {
policyName string
Expand Down Expand Up @@ -320,8 +285,7 @@ func plainStatusIndicator(status coverage.RequirementStatus) string {

// writeHuman formats the report as styled text with Unicode symbols and color.
func writeHuman(w io.Writer, report *coverage.Report) error {
fmt.Fprintln(w, styleTitle.Render(fmt.Sprintf("Coverage Report: %s", report.PolicyID)))
fmt.Fprintln(w, styleDim.Render(strings.Repeat("━", 50)))
fmt.Fprintln(w, renderHeader(fmt.Sprintf("Coverage Report: %s", report.PolicyID)))

type controlGroup struct {
controlID string
Expand Down Expand Up @@ -364,7 +328,7 @@ func writeHuman(w io.Writer, report *coverage.Report) error {
}

fmt.Fprintln(w)
fmt.Fprintln(w, styleDim.Render(strings.Repeat("─", 50)))
fmt.Fprintln(w, renderSeparator())

covStyle := coverageStyle(report.Metrics.CoveragePercent)
fmt.Fprintf(w, " %s\n", covStyle.Render(
Expand Down
14 changes: 12 additions & 2 deletions cmd/complypack/cli/delta.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ func writeDeltaHuman(
w io.Writer,
report *requirement.DeltaReport,
) error {
// TODO: add styled output with lipgloss formatting.
return writeDeltaText(w, report)
fmt.Fprintln(w, renderHeader(fmt.Sprintf("Delta: %s", report.PolicyID)))
fmt.Fprintln(w, " "+renderMetadata("Catalogs Compared", len(report.CatalogsCompared)))
fmt.Fprintln(w, " "+renderMetadata("Comparisons", len(report.Comparisons)))

for _, c := range report.Comparisons {
fmt.Fprintln(w, fmt.Sprintf("\n %s / %s", styleControl.Render(c.RequirementID), styleDim.Render(c.Label)))
fmt.Fprintln(w, fmt.Sprintf(" %s", renderMetadata("Policy value", c.PolicyValue)))
if c.RequirementText != "" {
fmt.Fprintln(w, fmt.Sprintf(" %s", renderMetadata("Requirement", c.RequirementText)))
}
}
return nil
}
63 changes: 55 additions & 8 deletions cmd/complypack/cli/delta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,67 @@ func TestWriteDeltaJSON(t *testing.T) {
parsed.Comparisons[0].PolicyValue)
}

func TestWriteDeltaHuman_DelegatesToText(t *testing.T) {
func TestWriteDeltaHuman(t *testing.T) {
report := &requirement.DeltaReport{
PolicyID: "test-policy",
CatalogsCompared: []string{},
Comparisons: []requirement.ParameterComparison{},
CatalogsCompared: []string{"catalog-a", "catalog-b"},
Comparisons: []requirement.ParameterComparison{
{
RequirementID: "REQ-001",
Label: "tls_version",
PolicyValue: "1.2",
PolicySource: "test-policy",
RequirementText: "Must use TLS",
CatalogSource: "catalog-a",
},
},
}

var textBuf, humanBuf bytes.Buffer
err := writeDeltaText(&textBuf, report)
var buf bytes.Buffer
err := writeDeltaHuman(&buf, report)
require.NoError(t, err)

err = writeDeltaHuman(&humanBuf, report)
output := buf.String()

// Verify styled header
assert.Contains(t, output, "test-policy")
assert.Contains(t, output, "━",
"human output should contain styled header separator")

// Verify data passes through
assert.Contains(t, output, "Catalogs Compared:")
assert.Contains(t, output, "Comparisons:")
assert.Contains(t, output, "REQ-001")
assert.Contains(t, output, "tls_version")
assert.Contains(t, output, "Policy value:")
assert.Contains(t, output, "1.2")
assert.Contains(t, output, "Requirement:")
assert.Contains(t, output, "Must use TLS")

// Verify human output differs from text output
var textBuf bytes.Buffer
err = writeDeltaText(&textBuf, report)
require.NoError(t, err)

assert.Equal(t, textBuf.String(), humanBuf.String(),
"human format should delegate to text for now")
assert.NotEqual(t, textBuf.String(), buf.String(),
"human format should differ from text format")
}

func TestWriteDeltaHuman_NoComparisons(t *testing.T) {
report := &requirement.DeltaReport{
PolicyID: "empty-policy",
CatalogsCompared: []string{},
Comparisons: []requirement.ParameterComparison{},
}

var buf bytes.Buffer
err := writeDeltaHuman(&buf, report)
require.NoError(t, err)

output := buf.String()
assert.Contains(t, output, "empty-policy")
assert.Contains(t, output, "━",
"human output should contain styled header separator")
assert.Contains(t, output, "Catalogs Compared:")
assert.Contains(t, output, "Comparisons:")
}
16 changes: 14 additions & 2 deletions cmd/complypack/cli/requirements.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ func writeRequirementsHuman(
catalogName string,
results []requirement.AssessmentRequirementInfo,
) error {
// TODO: add styled output with lipgloss formatting.
return writeRequirementsText(w, catalogName, results)
fmt.Fprintln(w, renderHeader(fmt.Sprintf("Requirements: %s", catalogName)))
fmt.Fprintln(w, fmt.Sprintf(" %s", renderMetadata("Count", len(results))))

for _, r := range results {
fmt.Fprintf(w, "\n %s %s\n", styleControl.Render(r.ID), styleDim.Render(fmt.Sprintf("(control: %s)", r.ControlID)))
fmt.Fprintf(w, " %s\n", r.Text)
if len(r.Applicability) > 0 {
fmt.Fprintf(w, " %s %s\n", styleOK.Render("Applicability:"), strings.Join(r.Applicability, ", "))
}
for k, v := range r.Parameters {
fmt.Fprintln(w, " "+renderMetadata(k, v))
}
}
return nil
}
66 changes: 55 additions & 11 deletions cmd/complypack/cli/requirements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,26 +132,70 @@ func TestWriteRequirementsJSON_Empty(t *testing.T) {
assert.Empty(t, parsed)
}

func TestWriteRequirementsHuman_DelegatesToText(t *testing.T) {
func TestWriteRequirementsHuman(t *testing.T) {
results := []requirement.AssessmentRequirementInfo{
{
ID: "AR-001",
ControlID: "CTL-001",
Text: "Some text",
ID: "AR-001",
ControlID: "CTL-001",
Text: "Requirement text one",
Applicability: []string{"maturity-1"},
Parameters: map[string]string{"key": "val"},
},
{
ID: "AR-002",
ControlID: "CTL-002",
Text: "Requirement text two",
},
}

var textBuf, humanBuf bytes.Buffer
err := writeRequirementsText(
&textBuf, "cat", results,
var buf bytes.Buffer
err := writeRequirementsHuman(
&buf, "my-catalog", results,
)
require.NoError(t, err)

err = writeRequirementsHuman(
&humanBuf, "cat", results,
output := buf.String()

// Verify styled header
assert.Contains(t, output, "my-catalog")
assert.Contains(t, output, "━",
"human output should contain styled header separator")

// Verify data passes through
assert.Contains(t, output, "Count:")
assert.Contains(t, output, "AR-001")
assert.Contains(t, output, "CTL-001")
assert.Contains(t, output, "Requirement text one")
assert.Contains(t, output, "maturity-1")
assert.Contains(t, output, "key:")
assert.Contains(t, output, "val")
assert.Contains(t, output, "AR-002")
assert.Contains(t, output, "CTL-002")
assert.Contains(t, output, "Applicability:")

// Verify human output differs from text output
var textBuf bytes.Buffer
err = writeRequirementsText(
&textBuf, "my-catalog", results,
)
require.NoError(t, err)

assert.Equal(t, textBuf.String(), humanBuf.String(),
"human format should delegate to text for now")
assert.NotEqual(t, textBuf.String(), buf.String(),
"human format should differ from text format")
}

func TestWriteRequirementsHuman_Empty(t *testing.T) {
var buf bytes.Buffer
err := writeRequirementsHuman(
&buf, "empty-catalog",
[]requirement.AssessmentRequirementInfo{},
)
require.NoError(t, err)

output := buf.String()
assert.Contains(t, output, "empty-catalog")
assert.Contains(t, output, "━",
"human output should contain styled header separator")
assert.Contains(t, output, "Count:")
assert.NotContains(t, output, "Applicability:")
}
Loading