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

Boostrap oscal generator and subcommand #194

Merged
merged 6 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions cmd/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/ossf/security-baseline
go 1.23

require (
github.com/defenseunicorns/go-oscal v0.6.2
github.com/spf13/cobra v1.8.1
gopkg.in/yaml.v3 v3.0.1
)
Expand Down
2 changes: 2 additions & 0 deletions cmd/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/defenseunicorns/go-oscal v0.6.2 h1:oLkMAJYVMq73Rm+9efcyaKq5SLMditjB6wv7o3XXpq8=
github.com/defenseunicorns/go-oscal v0.6.2/go.mod h1:UHp2yK9ty2mYJDun7oNhbstCq6SAAwP4YGbw9n7uG6o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
94 changes: 94 additions & 0 deletions cmd/internal/cmd/oscal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: Copyright 2025 The OSPS Authors
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"errors"
"fmt"
"os"

"github.com/ossf/security-baseline/pkg/baseline"
"github.com/spf13/cobra"
)

var appname = "baseline"

type oscalOptions struct {
outPath string
baselinePath string
}

// Validate the options in context with arguments
func (o *oscalOptions) Validate() error {
errs := []error{}

if o.baselinePath == "" {
errs = append(errs, errors.New("baseline path not specified"))
}
return errors.Join(errs...)
}

func (o *oscalOptions) AddFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(
&o.baselinePath, "baseline", "b", "", "path to directory containing the baseline YAML data",
)

cmd.PersistentFlags().StringVarP(
&o.outPath, "out", "o", "", "path to output file (defaults to STDOUT)",
)
}

func addOscal(parentCmd *cobra.Command) {
opts := oscalOptions{}
packCmd := &cobra.Command{
Short: "writes the baseline definition to an oscal json catalog",
Long: fmt.Sprintf(`
%s oscal: Write the OSPS Baseline to oscal definitions.
This subcommand exports the OSPS Baseline data to OSCAL (Open Security Controls
Assessment Language). This lets automated tools understand the criteria set as
OSCAL controls.
`, appname),
Use: "oscal -o osps.oscal.json",
SilenceUsage: false,
SilenceErrors: true,
PreRunE: func(_ *cobra.Command, args []string) error {
if opts.outPath != "" && len(args) > 1 && opts.outPath != args[1] {
return fmt.Errorf("out path specified twice")
}

if len(args) > 1 {
opts.outPath = args[1]
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := opts.Validate(); err != nil {
return err
}

cmd.SilenceUsage = true

loader := baseline.NewLoader()
loader.DataPath = opts.baselinePath

bline, err := loader.Load()
if err != nil {
return err
}

// TODO: Open the output file

gen := baseline.NewGenerator()
if err := gen.ExportOSCAL(bline, os.Stdout); err != nil {
return err
}

return nil
},
}
opts.AddFlags(packCmd)
parentCmd.AddCommand(packCmd)
}
1 change: 1 addition & 0 deletions cmd/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func Execute() error {
// Add the subcommands
addCompile(rootCmd)
addValidate(rootCmd)
addOscal(rootCmd)

return rootCmd.Execute()
}
120 changes: 120 additions & 0 deletions cmd/pkg/baseline/generator_oscal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: Copyright 2025 The OSPS Authors
// SPDX-License-Identifier: Apache-2.0

package baseline

import (
"encoding/json"
"fmt"
"io"
"strings"
"time"

oscal "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3"
"github.com/ossf/security-baseline/pkg/types"
)

const (
VersionOSPS = "devel"
controlHREF = "https://baseline.openssf.org/versions/%s#%s"
catalogUUID = "8c222a23-fc7e-4ad8-b6dd-289014f07a9f"

// OpenSSFNS is the OSCAL namespace URI to define the baseline names.
OpenSSFNS = "http://baseline.openssf.org/ns/oscal"
)

func (g *Generator) ExportOSCAL(b *types.Baseline, w io.Writer) error {
n := time.Now()
catalog := oscal.Catalog{
UUID: catalogUUID,
Groups: nil,
Metadata: oscal.Metadata{
LastModified: n,
Links: &[]oscal.Link{
{
Href: fmt.Sprintf(controlHREF, VersionOSPS, ""),
Rel: "canonical",
},
},
OscalVersion: "1.1.3",
Published: &n,
Title: "Open Source Project Security Baseline",
Version: VersionOSPS,
},
}

catalogGroups := []oscal.Group{}

for code, cat := range b.Categories {
group := oscal.Group{
Class: "OSPS",
Controls: nil,
ID: code,
Title: cat.Description,
}

controls := []oscal.Control{}

for _, control := range cat.Controls {

parts := []oscal.Part{}
for _, ar := range control.Requirements {
parts = append(parts, oscal.Part{
Class: control.ID,
ID: ar.ID,
Name: ar.ID,
Ns: "",
Parts: &[]oscal.Part{
{
ID: ar.ID + ".R",
Name: "recomemendation",
Ns: OpenSSFNS,
Prose: ar.Recommendation,
Links: &[]oscal.Link{
{
Href: fmt.Sprintf(controlHREF, VersionOSPS, ar.ID),
Rel: "canonical",
},
},
},
},
Prose: ar.Text,
Title: "",
})
}

newCtl := oscal.Control{
Class: code,
ID: control.ID,
Links: &[]oscal.Link{
{
Href: fmt.Sprintf(controlHREF, VersionOSPS, strings.ToLower(control.ID)),
Rel: "canonical",
},
},
Parts: &parts,
Title: strings.TrimSpace(control.Title),
}
controls = append(controls, newCtl)
}

group.Controls = &controls
catalogGroups = append(catalogGroups, group)
}
catalog.Groups = &catalogGroups

// Wrap the catalog to render the required "catalog" wrapper
// in the JSON file:
var wrapper = struct {
Catalog oscal.Catalog `json:"catalog"`
}{
Catalog: catalog,
}

enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(wrapper); err != nil {
return fmt.Errorf("encoding oscal json data: %w", err)
}
return nil
}