From 3f01f38c4906ee69b8c4d1d3a2cf43dbaf2804db Mon Sep 17 00:00:00 2001 From: Minhaj Uddin Khan Date: Wed, 13 Feb 2019 15:15:18 +0500 Subject: [PATCH 1/2] Increase code coverage --- generator/generator_test.go | 233 +++++++++++++++++++++++++++++++++ generator/mapper.go | 100 +++++++------- generator/profile.go | 5 + types/oscal/catalog/helpers.go | 13 ++ 4 files changed, 305 insertions(+), 46 deletions(-) diff --git a/generator/generator_test.go b/generator/generator_test.go index 8c08b734..555eefcf 100644 --- a/generator/generator_test.go +++ b/generator/generator_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "net/url" + "path/filepath" "testing" "github.com/docker/oscalkit/impl" @@ -438,6 +439,10 @@ func TestProcessSetParam(t *testing.T) { }, }, }, + profile.SetParam{ + Id: parameterID, + Constraints: []catalog.Constraint{}, + }, } controls := []catalog.Control{ catalog.Control{ @@ -520,3 +525,231 @@ func failTest(err error, t *testing.T) { t.Error(err) } } + +func TestMakeURL(t *testing.T) { + httpURI, _ := url.Parse("http://localhost:3000/v1/tests/profiles") + relpath, _ := url.Parse("../users") + + expectedOutput, _ := url.Parse("http://localhost:3000/v1/users") + + out, _ := makeURL(httpURI, relpath) + if expectedOutput.String() != out.String() { + t.Fail() + } +} + +func TestSetBasePathWithRelPath(t *testing.T) { + relativePath := "./something.xml" + absoulePath, _ := filepath.Abs(relativePath) + p := &profile.Profile{ + Imports: []profile.Import{ + profile.Import{ + Href: &catalog.Href{ + URL: func() *url.URL { + x, _ := url.Parse(relativePath) + return x + }(), + }, + }, + }, + } + p, err := SetBasePath(p, "") + if err != nil { + t.Error(err) + } + if p.Imports[0].Href.String() != absoulePath { + t.Fail() + } +} + +func TestSetBasePathWithHttpPath(t *testing.T) { + httpPath := "http://localhost:3000/v1/test/profiles/p1.xml" + relativePath := "./p2.xml" + outputPath := "http://localhost:3000/v1/test/profiles/p2.xml" + p := &profile.Profile{ + Imports: []profile.Import{ + profile.Import{ + Href: &catalog.Href{ + URL: func() *url.URL { + x, _ := url.Parse(relativePath) + return x + }(), + }, + }, + profile.Import{ + Href: &catalog.Href{ + URL: func() *url.URL { + x, _ := url.Parse(httpPath) + return x + }(), + }, + }, + }, + } + p, err := SetBasePath(p, httpPath) + if err != nil { + t.Error(err) + } + if p.Imports[0].Href.String() != outputPath { + t.Fail() + } + if p.Imports[1].Href.String() != httpPath { + t.Fail() + } +} + +func TestGetAltersWithAltersPresent(t *testing.T) { + + alterTitle := catalog.Title("test-title") + ctrlID := "ctrl-1" + p := &profile.Profile{ + Imports: []profile.Import{ + profile.Import{ + Href: &catalog.Href{ + URL: func() *url.URL { + u, _ := url.Parse("p1.xml") + return u + }(), + }, + Include: &profile.Include{ + IdSelectors: []profile.Call{ + profile.Call{ + ControlId: ctrlID, + }, + }, + }, + }, + }, + Modify: &profile.Modify{ + Alterations: []profile.Alter{ + profile.Alter{ + ControlId: ctrlID, + Additions: []profile.Add{ + profile.Add{ + Title: alterTitle, + }, + }, + }, + }, + }, + } + + alters, err := GetAlters(p) + if err != nil { + t.Error(err) + } + if len(alters) == 0 { + t.Error("no alters found") + } + if alters[0].ControlId != ctrlID { + t.Fail() + } +} + +func TestAddSubControlToControls(t *testing.T) { + controlDetails := catalog.Control{Id: "ac-2"} + subCtrlToAdd := catalog.Subcontrol{Id: "ac-2.1"} + g := catalog.Group{ + Controls: []catalog.Control{catalog.Control{Id: "ac-1"}}, + } + AddSubControlToControls(&g, controlDetails, subCtrlToAdd, &impl.NISTCatalog{}) + ctrlFound := false + for _, x := range g.Controls { + if x.Id == "ac-2" { + ctrlFound = true + subCtrlFound := false + for _, y := range x.Subcontrols { + if y.Id == "ac-2.1" { + subCtrlFound = true + } + } + if !subCtrlFound { + t.Fail() + } + break + } + } + if !ctrlFound { + t.Fail() + } +} + +func TestAddExistingControlToGroup(t *testing.T) { + ctrlToAdd := catalog.Control{Id: "ac-1"} + g := catalog.Group{ + Controls: []catalog.Control{ + catalog.Control{ + Id: "ac-1", + }, + }, + } + AddControlToGroup(&g, ctrlToAdd, &impl.NISTCatalog{}) + if len(g.Controls) > 1 { + t.Fail() + } +} + +func TestInvalidGetSubControl(t *testing.T) { + c := profile.Call{SubcontrolId: "ac-2.1"} + controls := []catalog.Control{catalog.Control{Id: "ac-1"}} + _, err := getSubControl(c, controls, &impl.NISTCatalog{}) + if err == nil { + t.Fail() + } +} + +func TestGetMappedCatalogControlsFromImport(t *testing.T) { + importedCatalog := catalog.Catalog{ + Groups: []catalog.Group{ + catalog.Group{ + Controls: []catalog.Control{ + catalog.Control{ + Id: "ac-2", + }, + }, + }, + }, + } + profileImport := profile.Import{ + Include: &profile.Include{ + IdSelectors: []profile.Call{ + profile.Call{ + SubcontrolId: "ac-2.1", + }, + }, + }, + } + _, err := GetMappedCatalogControlsFromImport(&importedCatalog, profileImport, &impl.NISTCatalog{}) + if err == nil { + t.Fail() + } +} + +func TestValidateHref(t *testing.T) { + err := ValidateHref(&catalog.Href{URL: &url.URL{RawPath: ":'//:://"}}) + if err != nil { + t.Fail() + } +} +func TestNilHref(t *testing.T) { + if err := ValidateHref(nil); err == nil { + t.Fail() + } +} + +func TestInvalidCreateCatalogsFromProfile(t *testing.T) { + p := profile.Profile{ + Imports: []profile.Import{ + profile.Import{Href: &catalog.Href{URL: &url.URL{RawPath: ":'//:://"}}}, + }, + } + _, err := CreateCatalogsFromProfile(&p) + if err == nil { + t.Fail() + } +} + +func TestModifyParts(t *testing.T) { + + ModifyParts(catalog.Part{}, []catalog.Part{}) +} diff --git a/generator/mapper.go b/generator/mapper.go index ce8ee9bc..16e3580e 100644 --- a/generator/mapper.go +++ b/generator/mapper.go @@ -96,66 +96,74 @@ func getSubControl(call profile.Call, ctrls []catalog.Control, helper impl.Catal return catalog.Subcontrol{}, fmt.Errorf("could not find subcontrol %s in catalog", call.SubcontrolId) } +func doesCallContainSubcontrol(c profile.Call) bool { + return c.ControlId == "" +} + +// AddControlToGroup adds control to a group +func AddControlToGroup(g *catalog.Group, ctrl catalog.Control, catalogHelper impl.Catalog) { + ctrlExists := false + for _, x := range g.Controls { + if x.Id == ctrl.Id { + ctrlExists = true + continue + } + } + if !ctrlExists { + g.Controls = append(g.Controls, + catalog.Control{ + Id: ctrl.Id, + Class: ctrl.Class, + Title: ctrl.Title, + Subcontrols: []catalog.Subcontrol{}, + Params: ctrl.Params, + Parts: ctrl.Parts, + }, + ) + } +} + +// AddSubControlToControls adds subcontrols to a group. If parent ctrl of subctrl doesn't exists, it adds its parent ctrl as well +func AddSubControlToControls(g *catalog.Group, ctrl catalog.Control, sc catalog.Subcontrol, catalogHelper impl.Catalog) { + ctrlExistsInGroup := false + for i, mappedCtrl := range g.Controls { + if mappedCtrl.Id == strings.ToLower(catalogHelper.GetControl(sc.Id)) { + ctrlExistsInGroup = true + g.Controls[i].Subcontrols = append(g.Controls[i].Subcontrols, sc) + } + } + if !ctrlExistsInGroup { + g.Controls = append(g.Controls, + catalog.Control{ + Id: ctrl.Id, + Class: ctrl.Class, + Title: ctrl.Title, + Params: ctrl.Params, + Parts: ctrl.Parts, + Subcontrols: []catalog.Subcontrol{sc}, + }) + } +} + // GetMappedCatalogControlsFromImport gets mapped controls in catalog per profile import func GetMappedCatalogControlsFromImport(importedCatalog *catalog.Catalog, profileImport profile.Import, catalogHelper impl.Catalog) (catalog.Catalog, error) { - newCatalog := catalog.Catalog{ - Title: importedCatalog.Title, - Groups: []catalog.Group{}, - } + newCatalog := catalog.CreateCatalog(importedCatalog.Title, []catalog.Group{}) for _, group := range importedCatalog.Groups { - newGroup := catalog.Group{ - Title: group.Title, - Controls: []catalog.Control{}, - } + newGroup := catalog.CreateGroup(group.Title, []catalog.Control{}) for _, ctrl := range group.Controls { for _, call := range profileImport.Include.IdSelectors { - if call.ControlId == "" { + if doesCallContainSubcontrol(call) { if strings.ToLower(ctrl.Id) == strings.ToLower(catalogHelper.GetControl(call.SubcontrolId)) { - ctrlExistsInGroup := false sc, err := getSubControl(call, group.Controls, &impl.NISTCatalog{}) if err != nil { return catalog.Catalog{}, err } - for i, mappedCtrl := range newGroup.Controls { - if mappedCtrl.Id == strings.ToLower(catalogHelper.GetControl(call.SubcontrolId)) { - ctrlExistsInGroup = true - newGroup.Controls[i].Subcontrols = append(newGroup.Controls[i].Subcontrols, sc) - } - } - if !ctrlExistsInGroup { - newGroup.Controls = append(newGroup.Controls, - catalog.Control{ - Id: ctrl.Id, - Class: ctrl.Class, - Title: ctrl.Title, - Params: ctrl.Params, - Parts: ctrl.Parts, - Subcontrols: []catalog.Subcontrol{sc}, - }) - } + AddSubControlToControls(&newGroup, ctrl, sc, catalogHelper) } } if strings.ToLower(call.ControlId) == strings.ToLower(ctrl.Id) { - ctrlExists := false - for _, x := range newGroup.Controls { - if x.Id == ctrl.Id { - ctrlExists = true - continue - } - } - if !ctrlExists { - newGroup.Controls = append(newGroup.Controls, - catalog.Control{ - Id: ctrl.Id, - Class: ctrl.Class, - Title: ctrl.Title, - Subcontrols: []catalog.Subcontrol{}, - Params: ctrl.Params, - Parts: ctrl.Parts, - }, - ) - } + AddControlToGroup(&newGroup, ctrl, catalogHelper) } } } diff --git a/generator/profile.go b/generator/profile.go index 0c72a488..13076f48 100644 --- a/generator/profile.go +++ b/generator/profile.go @@ -102,6 +102,11 @@ func GetAlters(p *profile.Profile) ([]profile.Alter, error) { var alterations []profile.Alter for _, i := range p.Imports { + if i.Include == nil { + i.Include = &profile.Include{ + IdSelectors: []profile.Call{}, + } + } for _, call := range i.Include.IdSelectors { found := false if p.Modify == nil { diff --git a/types/oscal/catalog/helpers.go b/types/oscal/catalog/helpers.go index 1c5905a8..c372d2e9 100644 --- a/types/oscal/catalog/helpers.go +++ b/types/oscal/catalog/helpers.go @@ -31,3 +31,16 @@ func NewControl(id, title string, opts *ControlOpts) Control { } return ctrl } + +func CreateCatalog(title Title, groups []Group) Catalog { + return Catalog{ + Title: title, + Groups: groups, + } +} +func CreateGroup(title Title, ctrls []Control) Group { + return Group{ + Title: title, + Controls: ctrls, + } +} From a271d849425cdac959734baeec024cbe47d1fd14 Mon Sep 17 00:00:00 2001 From: Minhaj Uddin Khan Date: Fri, 15 Feb 2019 16:27:47 +0500 Subject: [PATCH 2/2] Refactor code to enable unit testing --- cli/cmd/generate/catalog.go | 5 +- cli/cmd/generate/code.go | 4 +- generator/{profile.go => alter.go} | 80 ++- generator/generator_test.go | 782 +++++++++++++++++------------ generator/import.go | 59 +++ generator/manipulation.go | 106 ---- generator/mapper.go | 102 +--- generator/mocks.go | 69 +++ generator/processor.go | 225 +++++++++ generator/validator.go | 15 +- 10 files changed, 882 insertions(+), 565 deletions(-) rename generator/{profile.go => alter.go} (60%) create mode 100644 generator/import.go delete mode 100644 generator/manipulation.go create mode 100644 generator/mocks.go create mode 100644 generator/processor.go diff --git a/cli/cmd/generate/catalog.go b/cli/cmd/generate/catalog.go index 943ae810..07c97bb9 100644 --- a/cli/cmd/generate/catalog.go +++ b/cli/cmd/generate/catalog.go @@ -70,8 +70,9 @@ var Catalog = cli.Command{ if err != nil { return cli.NewExitError(fmt.Errorf("failed to setup href path for profiles: %v", err), 1) } - - catalogs, err := generator.CreateCatalogsFromProfile(profile) + processorFactory := generator.NewProcessorFactory() + alterHandler := generator.NewAlterHandler(*profile) + catalogs, err := generator.CreateCatalogsFromProfile(profile, generator.NewValidator(), generator.NewImportHandler(*profile), processorFactory, alterHandler) if err != nil { return cli.NewExitError(fmt.Sprintf("cannot create catalogs from profile, err: %v", err), 1) } diff --git a/cli/cmd/generate/code.go b/cli/cmd/generate/code.go index a9e0cb09..742c499d 100644 --- a/cli/cmd/generate/code.go +++ b/cli/cmd/generate/code.go @@ -81,7 +81,9 @@ var Code = cli.Command{ if err != nil { return cli.NewExitError(fmt.Errorf("failed to setup href path for profiles: %v", err), 1) } - catalogs, err := generator.CreateCatalogsFromProfile(profile) + processorFactory := generator.NewProcessorFactory() + alterHandler := generator.NewAlterHandler(*profile) + catalogs, err := generator.CreateCatalogsFromProfile(profile, generator.NewValidator(), generator.NewImportHandler(*profile), processorFactory, alterHandler) if err != nil { return cli.NewExitError(fmt.Sprintf("cannot create catalogs from profile, err: %v", err), 1) } diff --git a/generator/profile.go b/generator/alter.go similarity index 60% rename from generator/profile.go rename to generator/alter.go index 13076f48..202c6be4 100644 --- a/generator/profile.go +++ b/generator/alter.go @@ -5,11 +5,9 @@ import ( "net/url" "os" "path" - "path/filepath" "sync" "github.com/docker/oscalkit/types/oscal" - "github.com/docker/oscalkit/types/oscal/catalog" "github.com/docker/oscalkit/types/oscal/profile" ) @@ -23,7 +21,23 @@ var pathmap = HTTPFilePath{ m: make(map[string]string), } -func findAlter(p *profile.Profile, call profile.Call) (*profile.Alter, bool, error) { +type AlterHandler interface { + GetAlters() ([]profile.Alter, error) +} + +type altHandler struct { + pro profile.Profile + v Validator +} + +func NewAlterHandler(profile profile.Profile) AlterHandler { + return &altHandler{ + pro: profile, + v: NewValidator(), + } +} + +func (a *altHandler) findAlter(p *profile.Profile, call profile.Call) (*profile.Alter, bool, error) { if p.Modify == nil { p.Modify = &profile.Modify{ @@ -32,12 +46,12 @@ func findAlter(p *profile.Profile, call profile.Call) (*profile.Alter, bool, err } } for _, alt := range p.Modify.Alterations { - if EquateAlter(alt, call) { + if equateAlter(alt, call) { return &alt, true, nil } } for _, imp := range p.Imports { - err := ValidateHref(imp.Href) + err := a.v.ValidateHref(imp.Href) if err != nil { return nil, false, err } @@ -73,7 +87,7 @@ func findAlter(p *profile.Profile, call profile.Call) (*profile.Alter, bool, err return nil, false, err } o.Profile = p - alt, found, err := findAlter(o.Profile, call) + alt, found, err := a.findAlter(o.Profile, call) if err != nil { return nil, false, err } @@ -86,7 +100,7 @@ func findAlter(p *profile.Profile, call profile.Call) (*profile.Alter, bool, err } // EquateAlter equates alter with call -func EquateAlter(alt profile.Alter, call profile.Call) bool { +func equateAlter(alt profile.Alter, call profile.Call) bool { if alt.ControlId == "" && alt.SubcontrolId == call.SubcontrolId { return true @@ -98,10 +112,10 @@ func EquateAlter(alt profile.Alter, call profile.Call) bool { } // GetAlters gets alter attributes from import chain -func GetAlters(p *profile.Profile) ([]profile.Alter, error) { +func (a *altHandler) GetAlters() ([]profile.Alter, error) { var alterations []profile.Alter - for _, i := range p.Imports { + for _, i := range a.pro.Imports { if i.Include == nil { i.Include = &profile.Include{ IdSelectors: []profile.Call{}, @@ -109,21 +123,21 @@ func GetAlters(p *profile.Profile) ([]profile.Alter, error) { } for _, call := range i.Include.IdSelectors { found := false - if p.Modify == nil { - p.Modify = &profile.Modify{ + if a.pro.Modify == nil { + a.pro.Modify = &profile.Modify{ Alterations: []profile.Alter{}, ParamSettings: []profile.SetParam{}, } } - for _, alt := range p.Modify.Alterations { - if EquateAlter(alt, call) { + for _, alt := range a.pro.Modify.Alterations { + if equateAlter(alt, call) { alterations = append(alterations, alt) found = true break } } if !found { - alt, found, err := findAlter(p, call) + alt, found, err := a.findAlter(&a.pro, call) if err != nil { return nil, err } @@ -139,44 +153,6 @@ func GetAlters(p *profile.Profile) ([]profile.Alter, error) { } -// SetBasePath sets up base paths for profiles -func SetBasePath(p *profile.Profile, parentPath string) (*profile.Profile, error) { - for i, x := range p.Imports { - err := ValidateHref(x.Href) - if err != nil { - return nil, err - } - parentURL, err := url.Parse(parentPath) - if err != nil { - return nil, err - } - // If the import href is http. Do nothing as it doesn't depend on the parent path - if isHTTPResource(x.Href.URL) { - continue - } - //if parent is HTTP, and imports are relative, modify imports to http - if !isHTTPResource(x.Href.URL) && isHTTPResource(parentURL) { - url, err := makeURL(parentURL, x.Href.URL) - if err != nil { - return nil, err - } - p.Imports[i].Href = &catalog.Href{URL: url} - continue - } - path := fmt.Sprintf("%s/%s", path.Dir(parentPath), path.Base(x.Href.String())) - path, err = filepath.Abs(path) - if err != nil { - return nil, err - } - uri, err := url.Parse(path) - if err != nil { - return nil, err - } - p.Imports[i].Href = &catalog.Href{URL: uri} - } - return p, nil -} - func makeURL(url, child *url.URL) (*url.URL, error) { newURL, err := url.Parse(fmt.Sprintf("%s://%s%s/%s", url.Scheme, url.Host, path.Dir(url.Path), child.String())) if err != nil { diff --git a/generator/generator_test.go b/generator/generator_test.go index 555eefcf..80964615 100644 --- a/generator/generator_test.go +++ b/generator/generator_test.go @@ -9,269 +9,114 @@ import ( "github.com/docker/oscalkit/impl" "github.com/docker/oscalkit/types/oscal/catalog" - "github.com/docker/oscalkit/types/oscal/profile" -) -const ( - temporaryFilePathForCatalogJSON = "/tmp/catalog.json" - temporaryFilePathForProfileJSON = "/tmp/profile.json" - temporaryFilePathForCatalogsGoFile = "/tmp/catalogs.go" + "github.com/docker/oscalkit/types/oscal/profile" ) -func TestIsHttp(t *testing.T) { - - httpRoute := "http://localhost:3000" - expectedOutputForHTTP := true - - nonHTTPRoute := "NIST.GOV.JSON" - expectedOutputForNonHTTP := false - - r, err := url.Parse(httpRoute) - if err != nil { - t.Error(err) - } - if isHTTPResource(r) != expectedOutputForHTTP { - t.Error("Invalid output for http routes") - } - - r, err = url.Parse(nonHTTPRoute) - if err != nil { - t.Error(err) - } - if isHTTPResource(r) != expectedOutputForNonHTTP { - t.Error("Invalid output for non http routes") - } - -} - -func TestReadCatalog(t *testing.T) { - - catalogTitle := "NIST SP800-53" - r := bytes.NewReader([]byte(string( - fmt.Sprintf(` - { - "catalog": { - "title": "%s", - "declarations": { - "href": "NIST_SP-800-53_rev4_declarations.xml" - }, - "groups": [ - { - "controls": [ - { - "id": "at-1", - "class": "SP800-53", - "title": "Security Awareness and Training Policy and Procedures", - "params": [ - { - "id": "at-1_prm_1", - "label": "organization-defined personnel or roles" - }, - { - "id": "at-1_prm_2", - "label": "organization-defined frequency" - }, - { - "id": "at-1_prm_3", - "label": "organization-defined frequency" - } - ] - } - ] - } - ] - } - }`, catalogTitle)))) - - c, err := ReadCatalog(r) - if err != nil { - t.Error(err) - } - - if c.Title != catalog.Title(catalogTitle) { - t.Error("title not equal") - } - -} - -func TestReadInvalidCatalog(t *testing.T) { - - r := bytes.NewReader([]byte(string(`{ "catalog": "some dummy bad json"}`))) - _, err := ReadCatalog(r) - if err == nil { - t.Error("successfully parsed invalid catalog file") - } -} - -func TestCreateCatalogsFromProfile(t *testing.T) { - - href, _ := url.Parse("https://raw.githubusercontent.com/usnistgov/OSCAL/master/content/nist.gov/SP800-53/rev4/NIST_SP-800-53_rev4_catalog.xml") +func TestGetAlter(t *testing.T) { + url, _ := url.Parse("../test_util/artifacts/FedRAMP_LOW-baseline_profile.xml") + ctrlID := "ac-1" p := profile.Profile{ Imports: []profile.Import{ profile.Import{ - Href: &catalog.Href{ - URL: href, - }, + Href: &catalog.Href{URL: url}, Include: &profile.Include{ IdSelectors: []profile.Call{ profile.Call{ ControlId: "ac-1", }, + profile.Call{ + ControlId: "ac-2", + }, + profile.Call{ + ControlId: "ac-0", + }, }, }, }, }, Modify: &profile.Modify{ - Alterations: []profile.Alter{ - profile.Alter{ - ControlId: "ac-1", - Additions: []profile.Add{profile.Add{ - Parts: []catalog.Part{ - catalog.Part{ - Id: "ac-1_obj", - }, - }, - }}, - }, - }, + Alterations: []profile.Alter{profile.Alter{ + ControlId: "ac-1", + }}, }, } - x, err := CreateCatalogsFromProfile(&p) + altHandler := NewAlterHandler(p) + alts, err := altHandler.GetAlters() if err != nil { - t.Errorf("error should be null") + t.Error(err) + return } - if len(x) != 1 { - t.Error("there must be one catalog") + if len(alts) == 0 { + t.Fail() + return } - if x[0].Groups[0].Controls[0].Id != "ac-1" { - t.Error("Invalid control Id") + if alts[0].ControlId != ctrlID { + t.Fail() + return } - } +func TestMakeURL(t *testing.T) { + httpURI, _ := url.Parse("http://localhost:3000/v1/tests/profiles") + relpath, _ := url.Parse("../users") -func TestCreateCatalogsFromProfileWithBadHref(t *testing.T) { + expectedOutput, _ := url.Parse("http://localhost:3000/v1/users") - href, _ := url.Parse("this is a bad url") - p := profile.Profile{ - Imports: []profile.Import{ - profile.Import{ - Href: &catalog.Href{ - URL: href, - }, - Include: &profile.Include{ - IdSelectors: []profile.Call{ - profile.Call{ - ControlId: "ac-1", - }, - }, - }, - }, - }, - } - catalogs, err := CreateCatalogsFromProfile(&p) - if err == nil { - t.Error("error should not be nil") - } - if len(catalogs) > 0 { - t.Error("nothing should be parsed due to bad url") + out, _ := makeURL(httpURI, relpath) + if expectedOutput.String() != out.String() { + t.Fail() } } -func TestSubControlsMapping(t *testing.T) { +func TestValidateHref(t *testing.T) { - profile := profile.Profile{ - Imports: []profile.Import{ - profile.Import{ - Href: &catalog.Href{ - URL: func() *url.URL { - url, _ := url.Parse("https://raw.githubusercontent.com/usnistgov/OSCAL/master/content/nist.gov/SP800-53/rev4/NIST_SP-800-53_rev4_catalog.xml") - return url - }(), - }, - Include: &profile.Include{ - IdSelectors: []profile.Call{ - profile.Call{ - ControlId: "ac-1", - }, - profile.Call{ - ControlId: "ac-2", - }, - profile.Call{ - SubcontrolId: "ac-2.1", - }, - profile.Call{ - SubcontrolId: "ac-2.2", - }, - }, - }, - }, - }, - Modify: &profile.Modify{ - Alterations: []profile.Alter{ - profile.Alter{ - ControlId: "ac-1", - Additions: []profile.Add{profile.Add{ - Parts: []catalog.Part{ - catalog.Part{ - Id: "ac-1_obj", - }, - }, - }}, - }, - profile.Alter{ - ControlId: "ac-2", - Additions: []profile.Add{profile.Add{ - Parts: []catalog.Part{ - catalog.Part{ - Id: "ac-2_obj", - }, - }, - }}, - }, - profile.Alter{ - SubcontrolId: "ac-2.1", - Additions: []profile.Add{profile.Add{ - Parts: []catalog.Part{ - catalog.Part{ - Id: "ac-2.1_obj", - }, - }, - }}, - }, - profile.Alter{ - SubcontrolId: "ac-2.2", - Additions: []profile.Add{profile.Add{ - Parts: []catalog.Part{ - catalog.Part{ - Id: "ac-2.2_obj", - }, - }, - }}, - }, - }, - }, + err := NewValidator().ValidateHref(&catalog.Href{URL: &url.URL{RawPath: ":'//:://"}}) + if err != nil { + t.Fail() } - - c, err := CreateCatalogsFromProfile(&profile) +} +func TestNilHref(t *testing.T) { + if err := NewValidator().ValidateHref(nil); err == nil { + t.Fail() + } +} +func TestFindCatalog(t *testing.T) { + url, _ := url.Parse("../test_util/artifacts/FedRAMP_LOW-baseline_profile.xml") + profileImport := profile.Import{ + Href: &catalog.Href{URL: url}, + } + handler := NewImportHandler(profile.Profile{}) + c, err := handler.FindCatalog(profileImport) if err != nil { - t.Error("error should be nil") + t.Error(err) + return } - if c[0].Groups[0].Controls[1].Subcontrols[0].Id != "ac-2.1" { - t.Errorf("does not contain ac-2.1 in subcontrols") + if c == nil { + t.Fail() + return } - } -func TestGetCatalogInvalidFilePath(t *testing.T) { - - url := "http://[::1]a" - _, err := GetFilePath(url) +func TestFailingFindCatalogWithBadHref(t *testing.T) { + _, err := NewImportHandler(profile.Profile{}).FindCatalog(profile.Import{}) if err == nil { - t.Error("should fail") + t.Fail() + } +} +func TestFailingFindCatalogWithInvalidHref(t *testing.T) { + imp := profile.Import{ + Href: &catalog.Href{ + URL: &url.URL{}, + }, + } + _, err := NewImportHandler(profile.Profile{}).FindCatalog(imp) + if err == nil { + t.Fail() } } func TestProcessAdditionWithSameClass(t *testing.T) { + partID := "ac-10_prt" class := "guidance" alters := []profile.Alter{ @@ -330,8 +175,12 @@ func TestProcessAdditionWithSameClass(t *testing.T) { }, }, } - - o := ProcessAlterations(alters, &c) + processor, err := NewProcessor(&c, &impl.NISTCatalog{}) + if err != nil { + t.Error(err) + return + } + o := processor.ProcessAlterations(alters) for _, g := range o.Groups { for _, c := range g.Controls { for i := range c.Parts { @@ -414,7 +263,12 @@ func TestProcessAdditionWithDifferentPartClass(t *testing.T) { }, }, } - o := ProcessAlterations(alters, &c) + processor, err := NewProcessor(&c, &impl.NISTCatalog{}) + if err != nil { + t.Error(err) + return + } + o := processor.ProcessAlterations(alters) if len(o.Groups[0].Controls[0].Parts) != 2 { t.Error("parts for controls not getting added properly") } @@ -467,10 +321,14 @@ func TestProcessSetParam(t *testing.T) { }, }, } - nc := impl.NISTCatalog{} - ctlg = ProcessSetParam(sp, ctlg, &nc) - if ctlg.Groups[0].Controls[0].Parts[0].Prose.P[0].Raw != afterChange { - t.Error("failed to parse set param template") + processor, err := NewProcessor(ctlg, &impl.NISTCatalog{}) + if err != nil { + t.Error(err) + return + } + ctlg = processor.ProcessSetParam(sp) + if ctlg.Groups[0].Controls[0].Parts[0].Prose.P[0].Raw != afterChange { + t.Error("failed to parse set param template") } } @@ -513,31 +371,201 @@ func TestProcessSetParamWithUnmatchParam(t *testing.T) { }, }, } - nc := impl.NISTCatalog{} - ctlg = ProcessSetParam(sp, ctlg, &nc) + processor, err := NewProcessor(ctlg, &impl.NISTCatalog{}) + if err != nil { + t.Error(err) + return + } + ctlg = processor.ProcessSetParam(sp) if ctlg.Groups[0].Controls[0].Parts[0].Prose.P[0].Raw == afterChange { t.Error("should not change parameter with mismatching parameter id") } } -func failTest(err error, t *testing.T) { +func TestInvalidGetMappedCatalogControlsFromImport(t *testing.T) { + importedCatalog := catalog.Catalog{ + Groups: []catalog.Group{ + catalog.Group{ + Controls: []catalog.Control{ + catalog.Control{ + Id: "ac-2", + }, + }, + }, + }, + } + profileImport := profile.Import{ + Include: &profile.Include{ + IdSelectors: []profile.Call{ + profile.Call{ + SubcontrolId: "ac-2.1", + }, + }, + }, + } + processor, err := NewProcessor(&importedCatalog, &impl.NISTCatalog{}) if err != nil { t.Error(err) + return + } + _, err = processor.GetMappedCatalogControlsFromImport(profileImport) + if err == nil { + t.Fail() } } -func TestMakeURL(t *testing.T) { - httpURI, _ := url.Parse("http://localhost:3000/v1/tests/profiles") - relpath, _ := url.Parse("../users") +func TestGetMappedCatalogControlsFromImport(t *testing.T) { + importedCatalog := catalog.Catalog{ + Groups: []catalog.Group{ + catalog.Group{ + Controls: []catalog.Control{ + catalog.Control{ + Id: "ac-2", + Subcontrols: []catalog.Subcontrol{ + catalog.Subcontrol{ + Id: "ac-2.1", + }, + }, + }, + }, + }, + }, + } + profileImport := profile.Import{ + Include: &profile.Include{ + IdSelectors: []profile.Call{ + profile.Call{ + SubcontrolId: "ac-2.1", + }, + }, + }, + } + processor, err := NewProcessor(&importedCatalog, &impl.NISTCatalog{}) + if err != nil { + t.Error(err) + return + } + cat, err := processor.GetMappedCatalogControlsFromImport(profileImport) + if err != nil { + t.Fail() + return + } - expectedOutput, _ := url.Parse("http://localhost:3000/v1/users") + if cat.Groups[0].Controls[0].Id != "ac-2" { + t.Fail() + return + } + if cat.Groups[0].Controls[0].Subcontrols[0].Id != "ac-2.1" { + t.Fail() + return + } +} +func TestProcessProfile(t *testing.T) { + p := profile.Profile{ + Modify: &profile.Modify{ + Alterations: []profile.Alter{ + profile.Alter{ControlId: "ac-1"}, + }, + }, + } + c := catalog.Catalog{} + imp := profile.Import{} + importHandler := NewImportHandler(p) + processor := NewMockProcessor(&c, &impl.NISTCatalog{}) + alters := []profile.Alter{} + cat, err := importHandler.ProcessProfile(processor, alters, imp) + if err != nil { + t.Error(err) + return + } + if cat == nil { + t.Fail() + return + } +} +func TestAddSubControlToControls(t *testing.T) { + controlDetails := catalog.Control{Id: "ac-2"} + subCtrlToAdd := catalog.Subcontrol{Id: "ac-2.1"} + g := catalog.Group{ + Controls: []catalog.Control{catalog.Control{Id: "ac-1"}}, + } + AddSubControlToControls(&g, controlDetails, subCtrlToAdd, &impl.NISTCatalog{}) + ctrlFound := false + for _, x := range g.Controls { + if x.Id == "ac-2" { + ctrlFound = true + subCtrlFound := false + for _, y := range x.Subcontrols { + if y.Id == "ac-2.1" { + subCtrlFound = true + } + } + if !subCtrlFound { + t.Fail() + } + break + } + } + if !ctrlFound { + t.Fail() + } +} - out, _ := makeURL(httpURI, relpath) - if expectedOutput.String() != out.String() { +func TestAddExistingControlToGroup(t *testing.T) { + ctrlToAdd := catalog.Control{Id: "ac-1"} + g := catalog.Group{ + Controls: []catalog.Control{ + catalog.Control{ + Id: "ac-1", + }, + }, + } + AddControlToGroup(&g, ctrlToAdd, &impl.NISTCatalog{}) + if len(g.Controls) > 1 { + t.Fail() + } +} + +func TestInvalidGetSubControl(t *testing.T) { + c := profile.Call{SubcontrolId: "ac-2.1"} + controls := []catalog.Control{catalog.Control{Id: "ac-1"}} + _, err := getSubControl(c, controls, &impl.NISTCatalog{}) + if err == nil { t.Fail() } } +func TestGetSubControl(t *testing.T) { + c := profile.Call{SubcontrolId: "ac-2.1"} + controls := []catalog.Control{catalog.Control{Id: "ac-2", Subcontrols: []catalog.Subcontrol{ + catalog.Subcontrol{Id: "ac-2.1"}, + }}} + sc, err := getSubControl(c, controls, &impl.NISTCatalog{}) + if err != nil { + t.Error(err) + return + } + if sc.Id != "ac-2.1" { + t.Fail() + return + } +} + +func TestGetCatalogInvalidFilePath(t *testing.T) { + + url := "http://[::1]a" + _, err := GetFilePath(url) + if err == nil { + t.Error("should fail") + } +} + +func failTest(err error, t *testing.T) { + if err != nil { + t.Error(err) + } +} + func TestSetBasePathWithRelPath(t *testing.T) { relativePath := "./something.xml" absoulePath, _ := filepath.Abs(relativePath) @@ -634,7 +662,8 @@ func TestGetAltersWithAltersPresent(t *testing.T) { }, } - alters, err := GetAlters(p) + altHandler := NewAlterHandler(*p) + alters, err := altHandler.GetAlters() if err != nil { t.Error(err) } @@ -646,110 +675,223 @@ func TestGetAltersWithAltersPresent(t *testing.T) { } } -func TestAddSubControlToControls(t *testing.T) { - controlDetails := catalog.Control{Id: "ac-2"} - subCtrlToAdd := catalog.Subcontrol{Id: "ac-2.1"} - g := catalog.Group{ - Controls: []catalog.Control{catalog.Control{Id: "ac-1"}}, +func TestSubControlsMapping(t *testing.T) { + profile := profile.Profile{ + Imports: []profile.Import{ + profile.Import{ + Href: &catalog.Href{ + URL: func() *url.URL { + url, _ := url.Parse("https://raw.githubusercontent.com/usnistgov/OSCAL/master/content/nist.gov/SP800-53/rev4/NIST_SP-800-53_rev4_catalog.xml") + return url + }(), + }, + Include: &profile.Include{ + IdSelectors: []profile.Call{ + profile.Call{ + ControlId: "ac-1", + }, + profile.Call{ + ControlId: "ac-2", + }, + profile.Call{ + SubcontrolId: "ac-2.1", + }, + profile.Call{ + SubcontrolId: "ac-2.2", + }, + }, + }, + }, + }, + Modify: &profile.Modify{ + Alterations: []profile.Alter{ + profile.Alter{ + ControlId: "ac-1", + Additions: []profile.Add{profile.Add{ + Parts: []catalog.Part{ + catalog.Part{ + Id: "ac-1_obj", + }, + }, + }}, + }, + profile.Alter{ + ControlId: "ac-2", + Additions: []profile.Add{profile.Add{ + Parts: []catalog.Part{ + catalog.Part{ + Id: "ac-2_obj", + }, + }, + }}, + }, + profile.Alter{ + SubcontrolId: "ac-2.1", + Additions: []profile.Add{profile.Add{ + Parts: []catalog.Part{ + catalog.Part{ + Id: "ac-2.1_obj", + }, + }, + }}, + }, + profile.Alter{ + SubcontrolId: "ac-2.2", + Additions: []profile.Add{profile.Add{ + Parts: []catalog.Part{ + catalog.Part{ + Id: "ac-2.2_obj", + }, + }, + }}, + }, + }, + }, } - AddSubControlToControls(&g, controlDetails, subCtrlToAdd, &impl.NISTCatalog{}) - ctrlFound := false - for _, x := range g.Controls { - if x.Id == "ac-2" { - ctrlFound = true - subCtrlFound := false - for _, y := range x.Subcontrols { - if y.Id == "ac-2.1" { - subCtrlFound = true - } - } - if !subCtrlFound { - t.Fail() - } - break - } + + v := NewMockValidator() + pf := NewProcessorFactory() + altHandler := NewMockAlterHandler(profile) + c, err := CreateCatalogsFromProfile(&profile, v, NewImportHandler(profile), pf, altHandler) + if err != nil { + t.Error("error should be nil") } - if !ctrlFound { - t.Fail() + if c[0].Groups[0].Controls[1].Subcontrols[0].Id != "ac-2.1" { + t.Errorf("does not contain ac-2.1 in subcontrols") } + } -func TestAddExistingControlToGroup(t *testing.T) { - ctrlToAdd := catalog.Control{Id: "ac-1"} - g := catalog.Group{ - Controls: []catalog.Control{ - catalog.Control{ - Id: "ac-1", - }, - }, +func TestIsHttp(t *testing.T) { + + httpRoute := "http://localhost:3000" + expectedOutputForHTTP := true + + nonHTTPRoute := "NIST.GOV.JSON" + expectedOutputForNonHTTP := false + + r, err := url.Parse(httpRoute) + if err != nil { + t.Error(err) } - AddControlToGroup(&g, ctrlToAdd, &impl.NISTCatalog{}) - if len(g.Controls) > 1 { - t.Fail() + if isHTTPResource(r) != expectedOutputForHTTP { + t.Error("Invalid output for http routes") } + + r, err = url.Parse(nonHTTPRoute) + if err != nil { + t.Error(err) + } + if isHTTPResource(r) != expectedOutputForNonHTTP { + t.Error("Invalid output for non http routes") + } + } -func TestInvalidGetSubControl(t *testing.T) { - c := profile.Call{SubcontrolId: "ac-2.1"} - controls := []catalog.Control{catalog.Control{Id: "ac-1"}} - _, err := getSubControl(c, controls, &impl.NISTCatalog{}) +func TestReadCatalog(t *testing.T) { + + catalogTitle := "NIST SP800-53" + r := bytes.NewReader([]byte(string( + fmt.Sprintf(` + { + "catalog": { + "title": "%s", + "declarations": { + "href": "NIST_SP-800-53_rev4_declarations.xml" + }, + "groups": [ + { + "controls": [ + { + "id": "at-1", + "class": "SP800-53", + "title": "Security Awareness and Training Policy and Procedures", + "params": [ + { + "id": "at-1_prm_1", + "label": "organization-defined personnel or roles" + }, + { + "id": "at-1_prm_2", + "label": "organization-defined frequency" + }, + { + "id": "at-1_prm_3", + "label": "organization-defined frequency" + } + ] + } + ] + } + ] + } + }`, catalogTitle)))) + + c, err := ReadCatalog(r) + if err != nil { + t.Error(err) + } + + if c.Title != catalog.Title(catalogTitle) { + t.Error("title not equal") + } + +} + +func TestReadInvalidCatalog(t *testing.T) { + + r := bytes.NewReader([]byte(string(`{ "catalog": "some dummy bad json"}`))) + _, err := ReadCatalog(r) if err == nil { - t.Fail() + t.Error("successfully parsed invalid catalog file") } } -func TestGetMappedCatalogControlsFromImport(t *testing.T) { - importedCatalog := catalog.Catalog{ - Groups: []catalog.Group{ - catalog.Group{ - Controls: []catalog.Control{ - catalog.Control{ - Id: "ac-2", +func TestCreateCatalogsFromProfile(t *testing.T) { + + href, _ := url.Parse("https://raw.githubusercontent.com/usnistgov/OSCAL/master/content/nist.gov/SP800-53/rev4/NIST_SP-800-53_rev4_catalog.xml") + p := profile.Profile{ + Imports: []profile.Import{ + profile.Import{ + Href: &catalog.Href{ + URL: href, + }, + Include: &profile.Include{ + IdSelectors: []profile.Call{ + profile.Call{ + ControlId: "ac-1", + }, }, }, }, }, - } - profileImport := profile.Import{ - Include: &profile.Include{ - IdSelectors: []profile.Call{ - profile.Call{ - SubcontrolId: "ac-2.1", + Modify: &profile.Modify{ + Alterations: []profile.Alter{ + profile.Alter{ + ControlId: "ac-1", + Additions: []profile.Add{profile.Add{ + Parts: []catalog.Part{ + catalog.Part{ + Id: "ac-1_obj", + }, + }, + }}, }, }, }, } - _, err := GetMappedCatalogControlsFromImport(&importedCatalog, profileImport, &impl.NISTCatalog{}) - if err == nil { - t.Fail() - } -} - -func TestValidateHref(t *testing.T) { - err := ValidateHref(&catalog.Href{URL: &url.URL{RawPath: ":'//:://"}}) + v := NewMockValidator() + pf := NewProcessorFactory() + altHandler := NewMockAlterHandler(p) + x, err := CreateCatalogsFromProfile(&p, v, NewImportHandler(p), pf, altHandler) if err != nil { - t.Fail() - } -} -func TestNilHref(t *testing.T) { - if err := ValidateHref(nil); err == nil { - t.Fail() + t.Errorf("error should be null") } -} - -func TestInvalidCreateCatalogsFromProfile(t *testing.T) { - p := profile.Profile{ - Imports: []profile.Import{ - profile.Import{Href: &catalog.Href{URL: &url.URL{RawPath: ":'//:://"}}}, - }, + if len(x) != 1 { + t.Error("there must be one catalog") } - _, err := CreateCatalogsFromProfile(&p) - if err == nil { - t.Fail() + if x[0].Groups[0].Controls[0].Id != "ac-1" { + t.Error("Invalid control Id") } -} - -func TestModifyParts(t *testing.T) { - ModifyParts(catalog.Part{}, []catalog.Part{}) } diff --git a/generator/import.go b/generator/import.go new file mode 100644 index 00000000..57a6feb4 --- /dev/null +++ b/generator/import.go @@ -0,0 +1,59 @@ +package generator + +import ( + "context" + + "github.com/docker/oscalkit/types/oscal/catalog" + "github.com/docker/oscalkit/types/oscal/profile" +) + +// ImportHandler ... +type ImportHandler interface { + FindCatalog(profile.Import) (*catalog.Catalog, error) + ProcessProfile(processor Processor, alterations []profile.Alter, profImport profile.Import) (*catalog.Catalog, error) +} + +// NewImportHandler ... +func NewImportHandler(profile profile.Profile) ImportHandler { + return &impHandler{ + profile: profile, + validator: NewValidator(), + } +} + +type impHandler struct { + profile profile.Profile + validator Validator +} + +func (ih *impHandler) ProcessProfile(processor Processor, alterations []profile.Alter, profImport profile.Import) (*catalog.Catalog, error) { + // Prepare a new catalog object to merge into the final List of OutputCatalogs + if ih.profile.Modify != nil { + processor.ProcessAlterations(alterations) + processor.ProcessSetParam(ih.profile.Modify.ParamSettings) + } + newCatalog, err := processor.GetMappedCatalogControlsFromImport(profImport) + if err != nil { + return nil, err + } + return &newCatalog, nil +} + +func (ih *impHandler) FindCatalog(profileImport profile.Import) (*catalog.Catalog, error) { + c := make(chan *catalog.Catalog) + e := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // ForEach Import's Href, Fetch the Catalog JSON file + err := ih.validator.ValidateHref(profileImport.Href) + if err != nil { + return nil, err + } + getCatalogForImport(ctx, profileImport, c, e, profileImport.Href.String(), ih.validator) + select { + case importedCatalog := <-c: + return importedCatalog, nil + case err := <-e: + return nil, err + } +} diff --git a/generator/manipulation.go b/generator/manipulation.go deleted file mode 100644 index 8fbed11c..00000000 --- a/generator/manipulation.go +++ /dev/null @@ -1,106 +0,0 @@ -package generator - -import ( - "fmt" - - "github.com/docker/oscalkit/impl" - "github.com/docker/oscalkit/types/oscal/catalog" - "github.com/docker/oscalkit/types/oscal/profile" -) - -// ProcessAddition processes additions of a profile -func ProcessAddition(alt profile.Alter, controls []catalog.Control) []catalog.Control { - for j, ctrl := range controls { - if ctrl.Id == alt.ControlId { - for _, add := range alt.Additions { - for _, p := range add.Parts { - appended := false - for _, catalogPart := range ctrl.Parts { - if p.Class == catalogPart.Class { - appended = true - // append with all the parts with matching classes - parts := ModifyParts(p, ctrl.Parts) - ctrl.Parts = parts - } - } - if !appended { - ctrl.Parts = append(ctrl.Parts, p) - } - } - } - controls[j] = ctrl - } - for k, subctrl := range controls[j].Subcontrols { - if subctrl.Id == alt.SubcontrolId { - for _, add := range alt.Additions { - for _, p := range add.Parts { - appended := false - for _, catalogPart := range subctrl.Parts { - if p.Class == catalogPart.Class { - appended = true - // append with all the parts - parts := ModifyParts(p, subctrl.Parts) - subctrl.Parts = parts - } - } - if !appended { - subctrl.Parts = append(subctrl.Parts, p) - } - } - - } - } - controls[j].Subcontrols[k] = subctrl - } - } - return controls -} - -// ProcessAlterations processes alteration section of a profile -func ProcessAlterations(alterations []profile.Alter, c *catalog.Catalog) *catalog.Catalog { - for _, alt := range alterations { - for i, g := range c.Groups { - c.Groups[i].Controls = ProcessAddition(alt, g.Controls) - } - } - return c -} - -// ProcessSetParam processes set-param of a profile -func ProcessSetParam(setParams []profile.SetParam, c *catalog.Catalog, catalogHelper impl.Catalog) *catalog.Catalog { - for _, sp := range setParams { - ctrlID := catalogHelper.GetControl(sp.Id) - for i, g := range c.Groups { - for j, catalogCtrl := range g.Controls { - if ctrlID == catalogCtrl.Id { - for k := range catalogCtrl.Parts { - if len(sp.Constraints) == 0 { - continue - } - c.Groups[i].Controls[j].Parts[k].ModifyProse(sp.Id, sp.Constraints[0].Value) - } - } - } - } - } - return c -} - -// ModifyParts modifies parts -func ModifyParts(p catalog.Part, controlParts []catalog.Part) []catalog.Part { - - // append with all the parts - var parts []catalog.Part - for i, part := range controlParts { - if p.Class != part.Class { - parts = append(parts, part) - continue - } - id := part.Id - part.Id = fmt.Sprintf("%s_%d", id, i+1) - parts = append(parts, part) - part.Id = fmt.Sprintf("%s_%d", id, i+2) - parts = append(parts, part) - } - return parts -} diff --git a/generator/mapper.go b/generator/mapper.go index 16e3580e..61c9a89b 100644 --- a/generator/mapper.go +++ b/generator/mapper.go @@ -11,77 +11,44 @@ import ( "github.com/docker/oscalkit/types/oscal" "github.com/docker/oscalkit/types/oscal/catalog" "github.com/docker/oscalkit/types/oscal/profile" + "github.com/sirupsen/logrus" ) +type Mapper interface{} +type mapper struct{} + // CreateCatalogsFromProfile maps profile controls to multiple catalogs -func CreateCatalogsFromProfile(profileArg *profile.Profile) ([]*catalog.Catalog, error) { +func CreateCatalogsFromProfile(profileArg *profile.Profile, v Validator, handler ImportHandler, pf ProcessorFactory, altHandler AlterHandler) ([]*catalog.Catalog, error) { t := time.Now() - done := 0 - errChan := make(chan error) - catalogChan := make(chan *catalog.Catalog) var outputCatalogs []*catalog.Catalog logrus.Info("fetching alterations...") - alterations, err := GetAlters(profileArg) + alterations, err := altHandler.GetAlters() if err != nil { return nil, err } logrus.Info("fetching alterations from import chain complete") - logrus.Debug("processing alteration and parameters... \nmapping to controls...") // Get first import of the profile (which is a catalog) for _, profileImport := range profileArg.Imports { - err := ValidateHref(profileImport.Href) + err := v.ValidateHref(profileImport.Href) if err != nil { return nil, err } - go func(profileImport profile.Import) { - catalogHelper := impl.NISTCatalog{} - c := make(chan *catalog.Catalog) - e := make(chan error) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // ForEach Import's Href, Fetch the Catalog JSON file - getCatalogForImport(ctx, profileImport, c, e, profileImport.Href.String()) - select { - case importedCatalog := <-c: - // Prepare a new catalog object to merge into the final List of OutputCatalogs - if profileArg.Modify != nil { - nc := impl.NISTCatalog{} - importedCatalog = ProcessAlterations(alterations, importedCatalog) - importedCatalog = ProcessSetParam(profileArg.Modify.ParamSettings, importedCatalog, &nc) - } - newCatalog, err := GetMappedCatalogControlsFromImport(importedCatalog, profileImport, &catalogHelper) - if err != nil { - errChan <- err - return - } - catalogChan <- &newCatalog - - case err := <-e: - errChan <- err - return - } - - }(profileImport) - - } - for { - select { - case err := <-errChan: + cat, err := handler.FindCatalog(profileImport) + if err != nil { return nil, err - case newCatalog := <-catalogChan: - done++ - if newCatalog != nil { - outputCatalogs = append(outputCatalogs, newCatalog) - } - if done == len(profileArg.Imports) { - logrus.Infof("successfully mapped controls in %f seconds", time.Since(t).Seconds()) - return outputCatalogs, nil - } } + processor := pf.NewProcessor(cat, &impl.NISTCatalog{}) + outputCatalog, err := handler.ProcessProfile(processor, alterations, profileImport) + if err != nil { + return nil, err + } + outputCatalogs = append(outputCatalogs, outputCatalog) } + logrus.Infof("successfully mapped controls in %f seconds", time.Since(t).Seconds()) + return outputCatalogs, nil } func getSubControl(call profile.Call, ctrls []catalog.Control, helper impl.Catalog) (catalog.Subcontrol, error) { for _, ctrl := range ctrls { @@ -145,38 +112,9 @@ func AddSubControlToControls(g *catalog.Group, ctrl catalog.Control, sc catalog. } } -// GetMappedCatalogControlsFromImport gets mapped controls in catalog per profile import -func GetMappedCatalogControlsFromImport(importedCatalog *catalog.Catalog, profileImport profile.Import, catalogHelper impl.Catalog) (catalog.Catalog, error) { - - newCatalog := catalog.CreateCatalog(importedCatalog.Title, []catalog.Group{}) - for _, group := range importedCatalog.Groups { - newGroup := catalog.CreateGroup(group.Title, []catalog.Control{}) - for _, ctrl := range group.Controls { - for _, call := range profileImport.Include.IdSelectors { - if doesCallContainSubcontrol(call) { - if strings.ToLower(ctrl.Id) == strings.ToLower(catalogHelper.GetControl(call.SubcontrolId)) { - sc, err := getSubControl(call, group.Controls, &impl.NISTCatalog{}) - if err != nil { - return catalog.Catalog{}, err - } - AddSubControlToControls(&newGroup, ctrl, sc, catalogHelper) - } - } - if strings.ToLower(call.ControlId) == strings.ToLower(ctrl.Id) { - AddControlToGroup(&newGroup, ctrl, catalogHelper) - } - } - } - if len(newGroup.Controls) > 0 { - newCatalog.Groups = append(newCatalog.Groups, newGroup) - } - } - return newCatalog, nil -} - -func getCatalogForImport(ctx context.Context, i profile.Import, c chan *catalog.Catalog, e chan error, basePath string) { +func getCatalogForImport(ctx context.Context, i profile.Import, c chan *catalog.Catalog, e chan error, basePath string, v Validator) { go func(i profile.Import) { - err := ValidateHref(i.Href) + err := v.ValidateHref(i.Href) if err != nil { e <- fmt.Errorf("href cannot be nil") return @@ -209,7 +147,7 @@ func getCatalogForImport(ctx context.Context, i profile.Import, c chan *catalog. o.Profile = newP for _, p := range o.Profile.Imports { go func(p profile.Import) { - getCatalogForImport(ctx, p, c, e, basePath) + getCatalogForImport(ctx, p, c, e, basePath, v) }(p) } }(i) diff --git a/generator/mocks.go b/generator/mocks.go new file mode 100644 index 00000000..b7d2e065 --- /dev/null +++ b/generator/mocks.go @@ -0,0 +1,69 @@ +package generator + +import ( + "github.com/docker/oscalkit/impl" + "github.com/docker/oscalkit/types/oscal/catalog" + "github.com/docker/oscalkit/types/oscal/profile" +) + +type mockValidator struct{} +type mockAltHandler struct { + pro profile.Profile + v Validator +} +type mockProcessor struct { + catalog *catalog.Catalog + catalogHelper impl.Catalog +} + +// NewMockProcessor ... +func NewMockProcessor(c *catalog.Catalog, helper impl.Catalog) Processor { + return &mockProcessor{catalog: c, catalogHelper: helper} +} +func (m *mockProcessor) ProcessAddition(profile.Alter, []catalog.Control) []catalog.Control { + return []catalog.Control{catalog.Control{Id: "ac-1"}} +} +func (m *mockProcessor) ProcessAlterations([]profile.Alter) *catalog.Catalog { + return &catalog.Catalog{} +} +func (m *mockProcessor) ProcessSetParam([]profile.SetParam) *catalog.Catalog { + return &catalog.Catalog{} +} +func (m *mockProcessor) ModifyParts(catalog.Part, []catalog.Part) []catalog.Part { + return []catalog.Part{catalog.Part{Id: "ac-1_prm_1"}} +} +func (m *mockProcessor) GetMappedCatalogControlsFromImport(profileImport profile.Import) (catalog.Catalog, error) { + return catalog.Catalog{}, nil +} +func (m *mockProcessor) GetCatalog() *catalog.Catalog { return &catalog.Catalog{} } +func (m *mockProcessor) Helper() impl.Catalog { return &impl.NISTCatalog{} } + +// NewMockAlterHandler ... +func NewMockAlterHandler(profile profile.Profile) AlterHandler { + return &mockAltHandler{ + pro: profile, + v: NewValidator(), + } +} + +func (m *mockAltHandler) GetAlters() ([]profile.Alter, error) { + return []profile.Alter{ + profile.Alter{ + ControlId: "ac-1", + }, + profile.Alter{ + SubcontrolId: "ac-1.1", + }, + }, nil +} + +// Validator validates profile attributes + +// NewValidator creates a new mock +func NewMockValidator() Validator { + return &mockValidator{} +} + +func (v *mockValidator) ValidateHref(href *catalog.Href) error { + return nil +} diff --git a/generator/processor.go b/generator/processor.go new file mode 100644 index 00000000..3d2e791b --- /dev/null +++ b/generator/processor.go @@ -0,0 +1,225 @@ +package generator + +import ( + "fmt" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/docker/oscalkit/impl" + "github.com/docker/oscalkit/types/oscal/catalog" + "github.com/docker/oscalkit/types/oscal/profile" +) + +// Processor ... +type Processor interface { + ProcessAddition(profile.Alter, []catalog.Control) []catalog.Control + ProcessAlterations([]profile.Alter) *catalog.Catalog + ProcessSetParam([]profile.SetParam) *catalog.Catalog + ModifyParts(catalog.Part, []catalog.Part) []catalog.Part + GetMappedCatalogControlsFromImport(profileImport profile.Import) (catalog.Catalog, error) + GetCatalog() *catalog.Catalog + Helper() impl.Catalog +} + +type processor struct { + catalog *catalog.Catalog + catalogHelper impl.Catalog +} + +// ProcessorFactory ... +type ProcessorFactory interface { + NewProcessor(c *catalog.Catalog, catalogHelper impl.Catalog) Processor +} + +// NewProcessorFactory .. +func NewProcessorFactory() ProcessorFactory { + return &pf{} +} + +type pf struct{} + +func (p *pf) NewProcessor(c *catalog.Catalog, helper impl.Catalog) Processor { + return &processor{catalog: c, catalogHelper: helper} +} + +// NewProcessor creates a new profile processor against a catalog +func NewProcessor(c *catalog.Catalog, catalogHelper impl.Catalog) (Processor, error) { + if c == nil || catalogHelper == nil { + return nil, fmt.Errorf("incomplete args to create a profile processor") + } + catalog := processor{catalog: c, catalogHelper: catalogHelper} + return &catalog, nil +} + +func (pro *processor) Helper() impl.Catalog { + return pro.catalogHelper +} +func (pro *processor) GetCatalog() *catalog.Catalog { + return pro.catalog +} + +// ProcessAddition processes additions of a profile +func (pro *processor) ProcessAddition(alt profile.Alter, controls []catalog.Control) []catalog.Control { + for j, ctrl := range controls { + if ctrl.Id == alt.ControlId { + for _, add := range alt.Additions { + for _, p := range add.Parts { + appended := false + for _, catalogPart := range ctrl.Parts { + if p.Class == catalogPart.Class { + appended = true + // append with all the parts with matching classes + parts := pro.ModifyParts(p, ctrl.Parts) + ctrl.Parts = parts + } + } + if !appended { + ctrl.Parts = append(ctrl.Parts, p) + } + } + } + controls[j] = ctrl + } + for k, subctrl := range controls[j].Subcontrols { + if subctrl.Id == alt.SubcontrolId { + for _, add := range alt.Additions { + for _, p := range add.Parts { + appended := false + for _, catalogPart := range subctrl.Parts { + if p.Class == catalogPart.Class { + appended = true + // append with all the parts + parts := pro.ModifyParts(p, subctrl.Parts) + subctrl.Parts = parts + } + } + if !appended { + subctrl.Parts = append(subctrl.Parts, p) + } + } + + } + } + controls[j].Subcontrols[k] = subctrl + } + } + return controls +} + +// ProcessAlterations processes alteration section of a profile +func (pro *processor) ProcessAlterations(alterations []profile.Alter) *catalog.Catalog { + for _, alt := range alterations { + for i, g := range pro.catalog.Groups { + pro.catalog.Groups[i].Controls = pro.ProcessAddition(alt, g.Controls) + } + } + return pro.catalog +} + +// ProcessSetParam processes set-param of a profile +func (pro *processor) ProcessSetParam(setParams []profile.SetParam) *catalog.Catalog { + for _, sp := range setParams { + ctrlID := pro.catalogHelper.GetControl(sp.Id) + for i, g := range pro.catalog.Groups { + for j, catalogCtrl := range g.Controls { + if ctrlID == catalogCtrl.Id { + for k := range catalogCtrl.Parts { + if len(sp.Constraints) == 0 { + continue + } + pro.catalog.Groups[i].Controls[j].Parts[k].ModifyProse(sp.Id, sp.Constraints[0].Value) + } + } + } + } + } + return pro.catalog +} + +// ModifyParts modifies parts +func (pro *processor) ModifyParts(p catalog.Part, controlParts []catalog.Part) []catalog.Part { + + // append with all the parts + var parts []catalog.Part + for i, part := range controlParts { + if p.Class != part.Class { + parts = append(parts, part) + continue + } + id := part.Id + part.Id = fmt.Sprintf("%s_%d", id, i+1) + parts = append(parts, part) + part.Id = fmt.Sprintf("%s_%d", id, i+2) + parts = append(parts, part) + } + return parts +} + +// SetBasePath sets up base paths for profiles +func SetBasePath(p *profile.Profile, parentPath string) (*profile.Profile, error) { + for i, x := range p.Imports { + err := NewValidator().ValidateHref(x.Href) + if err != nil { + return nil, err + } + parentURL, err := url.Parse(parentPath) + if err != nil { + return nil, err + } + // If the import href is http. Do nothing as it doesn't depend on the parent path + if isHTTPResource(x.Href.URL) { + continue + } + //if parent is HTTP, and imports are relative, modify imports to http + if !isHTTPResource(x.Href.URL) && isHTTPResource(parentURL) { + url, err := makeURL(parentURL, x.Href.URL) + if err != nil { + return nil, err + } + p.Imports[i].Href = &catalog.Href{URL: url} + continue + } + path := fmt.Sprintf("%s/%s", path.Dir(parentPath), path.Base(x.Href.String())) + path, err = filepath.Abs(path) + if err != nil { + return nil, err + } + uri, err := url.Parse(path) + if err != nil { + return nil, err + } + p.Imports[i].Href = &catalog.Href{URL: uri} + } + return p, nil +} + +// GetMappedCatalogControlsFromImport gets mapped controls in catalog per profile import +func (pro *processor) GetMappedCatalogControlsFromImport(profileImport profile.Import) (catalog.Catalog, error) { + + newCatalog := catalog.CreateCatalog(pro.catalog.Title, []catalog.Group{}) + for _, group := range pro.catalog.Groups { + newGroup := catalog.CreateGroup(group.Title, []catalog.Control{}) + for _, ctrl := range group.Controls { + for _, call := range profileImport.Include.IdSelectors { + if doesCallContainSubcontrol(call) { + if strings.ToLower(ctrl.Id) == strings.ToLower(pro.catalogHelper.GetControl(call.SubcontrolId)) { + sc, err := getSubControl(call, group.Controls, &impl.NISTCatalog{}) + if err != nil { + return catalog.Catalog{}, err + } + AddSubControlToControls(&newGroup, ctrl, sc, pro.catalogHelper) + } + } + if strings.ToLower(call.ControlId) == strings.ToLower(ctrl.Id) { + AddControlToGroup(&newGroup, ctrl, pro.catalogHelper) + } + } + } + if len(newGroup.Controls) > 0 { + newCatalog.Groups = append(newCatalog.Groups, newGroup) + } + } + return newCatalog, nil +} diff --git a/generator/validator.go b/generator/validator.go index 0c1235b3..c3149a97 100644 --- a/generator/validator.go +++ b/generator/validator.go @@ -7,11 +7,22 @@ import ( "github.com/docker/oscalkit/types/oscal/catalog" ) -func ValidateHref(href *catalog.Href) error { +// Validator validates profile attributes +type Validator interface { + ValidateHref(*catalog.Href) error +} + +// NewValidator ... +func NewValidator() Validator { + return &validator{} +} + +type validator struct{} + +func (v *validator) ValidateHref(href *catalog.Href) error { if href == nil { return fmt.Errorf("Href cannot be empty") } - _, err := url.Parse(href.String()) if err != nil { return err