Skip to content

Commit

Permalink
Boostrap oscal generator and subcommand (#194)
Browse files Browse the repository at this point in the history
* Boostrap oscal generator and subcommand

This command initializes the first oscal output and
generator for the baseline data.

Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>

* Use props instead of parts for maturity level

Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>

* Update OSCAL generator to new schema

Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>

* Fix validation of resulting OSCAL document

Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>

* Update cmd/pkg/baseline/generator_oscal.go

Co-authored-by: Al <[email protected]>
Signed-off-by: CRob <[email protected]>

---------

Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>
Signed-off-by: CRob <[email protected]>
Signed-off-by: Ben Cotton <[email protected]>
Co-authored-by: CRob <[email protected]>
Co-authored-by: Al <[email protected]>
Co-authored-by: Ben Cotton <[email protected]>
  • Loading branch information
4 people authored Feb 21, 2025
1 parent 6b0e12f commit b75b30f
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 0 deletions.
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.9.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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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: "recommendation",
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
}

0 comments on commit b75b30f

Please sign in to comment.