From 353014db122f343f887ac9dbbf1614d0d33ec985 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sat, 23 Jul 2016 19:14:33 -0400 Subject: [PATCH 01/18] refactor helper methods out of templater test Makes it easier to read. --- templater/templater_test.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/templater/templater_test.go b/templater/templater_test.go index d7b8a92..e3a58e4 100644 --- a/templater/templater_test.go +++ b/templater/templater_test.go @@ -1,6 +1,7 @@ package templater_test import ( + "bytes" "path/filepath" "github.com/opencontrol/fedramp-templater/opencontrols" @@ -10,16 +11,25 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/opencontrol/fedramp-templater/reporter" - "bytes" ) -func loadOpenControlData(openControlDir string) opencontrols.Data { +func loadSSP(name string) *ssp.Document { + sspPath := filepath.Join("..", "fixtures", name) + doc, err := ssp.Load(sspPath) + Expect(err).NotTo(HaveOccurred()) + + return doc +} + +func loadOpenControlFixture() opencontrols.Data { + openControlDir := filepath.Join("..", "fixtures", "opencontrols") openControlDir, err := filepath.Abs(openControlDir) Expect(err).NotTo(HaveOccurred()) openControlData, errors := opencontrols.LoadFrom(openControlDir) for _, err := range errors { Expect(err).NotTo(HaveOccurred()) } + return openControlData } @@ -34,22 +44,20 @@ func extractDiffReport(reporters []reporter.Reporter) string { var _ = Describe("Templater", func() { Describe("TemplatizeSSP", func() { It("fills in the Responsible Role fields", func() { - sspPath := filepath.Join("..", "fixtures", "FedRAMP_ac-2-1_v2.1.docx") - s, err := ssp.Load(sspPath) - Expect(err).NotTo(HaveOccurred()) - defer s.Close() - - openControlData := loadOpenControlData(filepath.Join("..", "fixtures", "opencontrols")) + doc := loadSSP("FedRAMP_ac-2-1_v2.1.docx") + defer doc.Close() + openControlData := loadOpenControlFixture() - err = TemplatizeSSP(s, openControlData) + err := TemplatizeSSP(doc, openControlData) Expect(err).NotTo(HaveOccurred()) - content := s.Content() + content := doc.Content() Expect(content).To(ContainSubstring(`Responsible Role: Amazon Elastic Compute Cloud: AWS Staff`)) }) }) + Describe("DiffSSP", func() { - It("should warn the user if the current SSP contains a responsible role that conflicts with the " + + It("should warn the user if the current SSP contains a responsible role that conflicts with the "+ "responsbile role in the YAML", func() { By("Loading the SSP with the Responsible Role being 'OpenControl Role Placeholder' " + @@ -61,7 +69,7 @@ var _ = Describe("Templater", func() { By("Loading the data from the opencontrol workspace with the Responsible Role being " + "'Amazon Elastic Compute Cloud: AWS Staff' for Control 'AC-2 (1)'") - openControlData := loadOpenControlData(filepath.Join("..", "fixtures", "opencontrols")) + openControlData := loadOpenControlFixture() By("Calling 'diff' on the SSP") diffInfo, err := DiffSSP(s, openControlData) From 17ce563ba0c50370edfdae878706cdfc71d87fe4 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Thu, 28 Jul 2016 21:52:46 -0400 Subject: [PATCH 02/18] remove redundant disclaimer about gokogiri version --- control/summary_table.go | 1 - control/summary_table_test.go | 3 +-- docx/helper/helper.go | 1 - glide.yaml | 1 + ssp/document.go | 3 +-- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/control/summary_table.go b/control/summary_table.go index 078854f..4f853e9 100644 --- a/control/summary_table.go +++ b/control/summary_table.go @@ -5,7 +5,6 @@ import ( "regexp" "strings" - // using fork because of https://github.com/moovweb/gokogiri/pull/93#issuecomment-215582446 "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/fedramp-templater/opencontrols" "github.com/opencontrol/fedramp-templater/reporter" diff --git a/control/summary_table_test.go b/control/summary_table_test.go index af5a1fc..f432627 100644 --- a/control/summary_table_test.go +++ b/control/summary_table_test.go @@ -5,9 +5,8 @@ import ( "os" "path/filepath" "text/template" - // using fork because of https://github.com/moovweb/gokogiri/pull/93#issuecomment-215582446 - "github.com/jbowtie/gokogiri/xml" + "github.com/jbowtie/gokogiri/xml" . "github.com/opencontrol/fedramp-templater/control" "github.com/opencontrol/fedramp-templater/docx/helper" "github.com/opencontrol/fedramp-templater/opencontrols" diff --git a/docx/helper/helper.go b/docx/helper/helper.go index a14ba41..138c68a 100644 --- a/docx/helper/helper.go +++ b/docx/helper/helper.go @@ -1,7 +1,6 @@ package helper import ( - // using fork because of https://github.com/moovweb/gokogiri/pull/93#issuecomment-215582446 "github.com/jbowtie/gokogiri" "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/doc-template/docx" diff --git a/glide.yaml b/glide.yaml index 3c62388..e9806a6 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,5 +1,6 @@ package: github.com/opencontrol/fedramp-templater import: +# using fork because of https://github.com/moovweb/gokogiri/pull/93#issuecomment-215582446 - package: github.com/jbowtie/gokogiri version: 94c317865760e3580e90e49c08ca54953ef1b7cb subpackages: diff --git a/ssp/document.go b/ssp/document.go index d0c578a..d112d54 100644 --- a/ssp/document.go +++ b/ssp/document.go @@ -1,10 +1,9 @@ package ssp import ( + "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/doc-template/docx" "github.com/opencontrol/fedramp-templater/docx/helper" - // using fork because of https://github.com/moovweb/gokogiri/pull/93#issuecomment-215582446 - "github.com/jbowtie/gokogiri/xml" ) // SummaryTablesXPath is the pattern used to find summary tables within an SSP's XML. From a75cd3b96af9d538f19a2f37d3d6d2f5384dc847 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Fri, 26 Aug 2016 20:25:57 -0400 Subject: [PATCH 03/18] share fixture helper functions between tests --- control/summary_table_test.go | 33 ++++----------------------- fixtures/fixtures.go | 43 +++++++++++++++++++++++++++++++++++ ssp/document_test.go | 14 +++--------- templater/templater_test.go | 34 ++++----------------------- 4 files changed, 56 insertions(+), 68 deletions(-) create mode 100644 fixtures/fixtures.go diff --git a/control/summary_table_test.go b/control/summary_table_test.go index f432627..9b5a3e6 100644 --- a/control/summary_table_test.go +++ b/control/summary_table_test.go @@ -2,15 +2,13 @@ package control_test import ( "bytes" - "os" - "path/filepath" "text/template" "github.com/jbowtie/gokogiri/xml" . "github.com/opencontrol/fedramp-templater/control" "github.com/opencontrol/fedramp-templater/docx/helper" - "github.com/opencontrol/fedramp-templater/opencontrols" "github.com/opencontrol/fedramp-templater/ssp" + "github.com/opencontrol/fedramp-templater/fixtures" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -22,7 +20,7 @@ type tableData struct { } func docFixture(control string) *xml.XmlDocument { - path := filepath.Join("..", "fixtures", "simplified_table.xml") + path := fixtures.FixturePath("simplified_table.xml") tpl, err := template.ParseFiles(path) Expect(err).ToNot(HaveOccurred()) @@ -44,32 +42,12 @@ func getTable(control string) xml.Node { return tables[0] } -func openControlFixturePath() string { - path := filepath.Join("..", "fixtures", "opencontrols") - path, err := filepath.Abs(path) - Expect(err).NotTo(HaveOccurred()) - _, err = os.Stat(path) - Expect(err).NotTo(HaveOccurred()) - - return path -} - -func openControlFixture() opencontrols.Data { - path := openControlFixturePath() - data, errors := opencontrols.LoadFrom(path) - for _, err := range errors { - Expect(err).NotTo(HaveOccurred()) - } - - return data -} - var _ = Describe("SummaryTable", func() { Describe("Fill", func() { It("fills in the Responsible Role for controls", func() { table := getTable("AC-2") st := SummaryTable{Root: table} - openControlData := openControlFixture() + openControlData := fixtures.LoadOpenControlFixture() st.Fill(openControlData) @@ -79,7 +57,7 @@ var _ = Describe("SummaryTable", func() { It("fills in the Responsible Role for control enhancements", func() { table := getTable("AC-2 (1)") st := SummaryTable{Root: table} - openControlData := openControlFixture() + openControlData := fixtures.LoadOpenControlFixture() st.Fill(openControlData) @@ -91,8 +69,7 @@ var _ = Describe("SummaryTable", func() { It("detects no diff when the value of responsible role is empty", func() { table := getTable("AC-2") st := SummaryTable{Root: table} - openControlData := openControlFixture() - + openControlData := fixtures.LoadOpenControlFixture() diff, err := st.Diff(openControlData) Expect(diff).To(Equal([]reporter.Reporter{})) diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go new file mode 100644 index 0000000..4087519 --- /dev/null +++ b/fixtures/fixtures.go @@ -0,0 +1,43 @@ +package fixtures + +import ( + "os" + "path/filepath" + + . "github.com/onsi/gomega" + "github.com/opencontrol/fedramp-templater/opencontrols" + "github.com/opencontrol/fedramp-templater/ssp" +) + +func FixturePath(name string) string { + path := filepath.Join("..", "fixtures", name) + path, err := filepath.Abs(path) + Expect(err).NotTo(HaveOccurred()) + // ensure the file/directory exists + _, err = os.Stat(path) + Expect(err).NotTo(HaveOccurred()) + + return path +} + +func OpenControlFixturePath() string { + return FixturePath("opencontrols") +} + +func LoadSSP(name string) *ssp.Document { + sspPath := FixturePath(name) + doc, err := ssp.Load(sspPath) + Expect(err).NotTo(HaveOccurred()) + + return doc +} + +func LoadOpenControlFixture() opencontrols.Data { + openControlDir := OpenControlFixturePath() + openControlData, errors := opencontrols.LoadFrom(openControlDir) + for _, err := range errors { + Expect(err).NotTo(HaveOccurred()) + } + + return openControlData +} diff --git a/ssp/document_test.go b/ssp/document_test.go index 88c5c4b..6d970d4 100644 --- a/ssp/document_test.go +++ b/ssp/document_test.go @@ -1,25 +1,17 @@ package ssp_test import ( - "path/filepath" - + "github.com/opencontrol/fedramp-templater/fixtures" . "github.com/opencontrol/fedramp-templater/ssp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) -func loadSSP(name string) *Document { - path := filepath.Join("..", "fixtures", name) - doc, err := Load(path) - Expect(err).NotTo(HaveOccurred()) - return doc -} - var _ = Describe("SSP", func() { Describe("Load", func() { It("gets the content from the doc", func() { - doc := loadSSP("FedRAMP_ac-2-1_v2.1.docx") + doc := fixtures.LoadSSP("FedRAMP_ac-2-1_v2.1.docx") defer doc.Close() Expect(doc.Content()).To(ContainSubstring("Control Enhancement")) @@ -33,7 +25,7 @@ var _ = Describe("SSP", func() { Describe("SummaryTables", func() { It("returns the tables", func() { - doc := loadSSP("FedRAMP_ac-2_v2.1.docx") + doc := fixtures.LoadSSP("FedRAMP_ac-2_v2.1.docx") defer doc.Close() tables, err := doc.SummaryTables() diff --git a/templater/templater_test.go b/templater/templater_test.go index e3a58e4..e24393c 100644 --- a/templater/templater_test.go +++ b/templater/templater_test.go @@ -2,10 +2,8 @@ package templater_test import ( "bytes" - "path/filepath" - "github.com/opencontrol/fedramp-templater/opencontrols" - "github.com/opencontrol/fedramp-templater/ssp" + "github.com/opencontrol/fedramp-templater/fixtures" . "github.com/opencontrol/fedramp-templater/templater" . "github.com/onsi/ginkgo" @@ -13,26 +11,6 @@ import ( "github.com/opencontrol/fedramp-templater/reporter" ) -func loadSSP(name string) *ssp.Document { - sspPath := filepath.Join("..", "fixtures", name) - doc, err := ssp.Load(sspPath) - Expect(err).NotTo(HaveOccurred()) - - return doc -} - -func loadOpenControlFixture() opencontrols.Data { - openControlDir := filepath.Join("..", "fixtures", "opencontrols") - openControlDir, err := filepath.Abs(openControlDir) - Expect(err).NotTo(HaveOccurred()) - openControlData, errors := opencontrols.LoadFrom(openControlDir) - for _, err := range errors { - Expect(err).NotTo(HaveOccurred()) - } - - return openControlData -} - func extractDiffReport(reporters []reporter.Reporter) string { report := &bytes.Buffer{} for _, rept := range reporters { @@ -44,9 +22,9 @@ func extractDiffReport(reporters []reporter.Reporter) string { var _ = Describe("Templater", func() { Describe("TemplatizeSSP", func() { It("fills in the Responsible Role fields", func() { - doc := loadSSP("FedRAMP_ac-2-1_v2.1.docx") + doc := fixtures.LoadSSP("FedRAMP_ac-2-1_v2.1.docx") defer doc.Close() - openControlData := loadOpenControlFixture() + openControlData := fixtures.LoadOpenControlFixture() err := TemplatizeSSP(doc, openControlData) @@ -62,14 +40,12 @@ var _ = Describe("Templater", func() { By("Loading the SSP with the Responsible Role being 'OpenControl Role Placeholder' " + "for Control 'AC-2 (1)'") - sspPath := filepath.Join("..", "fixtures", "FedRAMP_ac-2-1_v2.1.docx") - s, err := ssp.Load(sspPath) - Expect(err).NotTo(HaveOccurred()) + s := fixtures.LoadSSP("FedRAMP_ac-2-1_v2.1.docx") defer s.Close() By("Loading the data from the opencontrol workspace with the Responsible Role being " + "'Amazon Elastic Compute Cloud: AWS Staff' for Control 'AC-2 (1)'") - openControlData := loadOpenControlFixture() + openControlData := fixtures.LoadOpenControlFixture() By("Calling 'diff' on the SSP") diffInfo, err := DiffSSP(s, openControlData) From 76669290b7e371ecc03b4dbc602d0d165ca90f3e Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Thu, 28 Jul 2016 12:02:40 -0400 Subject: [PATCH 04/18] fix component fixture to match actual SSP narrative structure --- fixtures/opencontrols/components/EC2/component.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fixtures/opencontrols/components/EC2/component.yaml b/fixtures/opencontrols/components/EC2/component.yaml index 7d04aae..2feeb99 100644 --- a/fixtures/opencontrols/components/EC2/component.yaml +++ b/fixtures/opencontrols/components/EC2/component.yaml @@ -14,7 +14,10 @@ satisfies: implementation_status: partial control_origin: shared narrative: - - text: "Justification in narrative form for AC-2" + - key: "a" + text: "Justification in narrative form A for AC-2" + - key: "b" + text: "Justification in narrative form B for AC-2" standard_key: NIST-800-53 - control_key: AC-2 (1) covered_by: @@ -25,10 +28,7 @@ satisfies: implementation_status: partial control_origin: shared narrative: - - key: "a" - text: "Justification in narrative form A for AC-2 (1)" - - key: "b" - text: "Justification in narrative form B for AC-2 (1)" + - text: "Justification in narrative form for AC-2 (1)" standard_key: NIST-800-53 responsible_role: "AWS Staff" schema_version: 3.0.0 From c7a2dfadbda2c64786f4a24269cb19919945dd46 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Thu, 28 Jul 2016 21:37:33 -0400 Subject: [PATCH 05/18] add a method to retrieve the narrative tables --- ssp/document.go | 8 +++++++- ssp/document_test.go | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ssp/document.go b/ssp/document.go index d112d54..6962936 100644 --- a/ssp/document.go +++ b/ssp/document.go @@ -36,12 +36,18 @@ func Load(path string) (ssp *Document, err error) { return } -// SummaryTables returns the tables for the controls and the control enhancements. +// SummaryTables returns the summary tables for the controls and the control enhancements. func (s *Document) SummaryTables() (tables []xml.Node, err error) { // find the tables matching the provided headers, ignoring whitespace return s.xmlDoc.Search(SummaryTablesXPath) } +// NarrativeTables returns the narrative tables for the controls and the control enhancements. +func (s *Document) NarrativeTables() (tables []xml.Node, err error) { + // find the tables matching the provided headers, ignoring whitespace + return s.xmlDoc.Search("//w:tbl[contains(normalize-space(.), 'What is the solution and how is it implemented?')]") +} + // Content retrieves the text from within the Word document. func (s *Document) Content() string { return s.wordDoc.GetContent() diff --git a/ssp/document_test.go b/ssp/document_test.go index 6d970d4..bac8b35 100644 --- a/ssp/document_test.go +++ b/ssp/document_test.go @@ -34,4 +34,16 @@ var _ = Describe("SSP", func() { Expect(len(tables)).To(Equal(10)) }) }) + + Describe("NarrativeTables", func() { + It("returns the tables", func() { + doc := loadSSP("FedRAMP_ac-2_v2.1.docx") + defer doc.Close() + + tables, err := doc.NarrativeTables() + + Expect(err).NotTo(HaveOccurred()) + Expect(len(tables)).To(Equal(8)) + }) + }) }) From 3d012ac73be9328dfea5143d1a643ea0092b5380 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Thu, 28 Jul 2016 23:09:22 -0400 Subject: [PATCH 06/18] add a GetNarrative() method for opencontrols.Data --- opencontrols/data.go | 13 +++++++-- opencontrols/data_test.go | 39 +++++++++++++++++++++++++ opencontrols/opencontrols_suite_test.go | 13 +++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 opencontrols/data_test.go create mode 100644 opencontrols/opencontrols_suite_test.go diff --git a/opencontrols/data.go b/opencontrols/data.go index 1318ada..1b6f598 100644 --- a/opencontrols/data.go +++ b/opencontrols/data.go @@ -5,14 +5,16 @@ import ( "github.com/opencontrol/compliance-masonry/models" ) +const standardKey = "NIST-800-53" + // Data contains the OpenControl justification information. type Data struct { ocd docx.OpenControlDocx } // LoadFrom creates a new Data struct from the provided path to an `opencontrols/` directory. -func LoadFrom(path string) (data Data, errors []error) { - openControlData, errors := models.LoadData(path, "") +func LoadFrom(dirPath string) (data Data, errors []error) { + openControlData, errors := models.LoadData(dirPath, "") if len(errors) > 0 { return } @@ -24,5 +26,10 @@ func LoadFrom(path string) (data Data, errors []error) { // GetResponsibleRoles returns the responsible role information for each component matching the specified control. func (d *Data) GetResponsibleRoles(control string) string { - return d.ocd.FormatResponsibleRoles("NIST-800-53", control) + return d.ocd.FormatResponsibleRoles(standardKey, control) +} + +// GetNarrative returns the justification text for the specified control. Pass an empty string for `sectionKey` if you are looking for the overall narrative. +func (d *Data) GetNarrative(control string, sectionKey string) string { + return d.ocd.FormatNarrative(standardKey, control, sectionKey) } diff --git a/opencontrols/data_test.go b/opencontrols/data_test.go new file mode 100644 index 0000000..83ffa3a --- /dev/null +++ b/opencontrols/data_test.go @@ -0,0 +1,39 @@ +package opencontrols_test + +import ( + "path/filepath" + + . "github.com/opencontrol/fedramp-templater/opencontrols" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +// TODO share with templater_test +func loadOpenControlFixture() Data { + openControlDir := filepath.Join("..", "fixtures", "opencontrols") + openControlDir, err := filepath.Abs(openControlDir) + Expect(err).NotTo(HaveOccurred()) + openControlData, errors := LoadFrom(openControlDir) + for _, err := range errors { + Expect(err).NotTo(HaveOccurred()) + } + + return openControlData +} + +var _ = Describe("Data", func() { + Describe("GetNarrative", func() { + It("returns the relevant singular narrative", func() { + data := loadOpenControlFixture() + result := data.GetNarrative("AC-2 (1)", "") + Expect(result).To(Equal("Amazon Elastic Compute Cloud\nJustification in narrative form for AC-2 (1)\n")) + }) + + It("returns the relevant narrative section", func() { + data := loadOpenControlFixture() + result := data.GetNarrative("AC-2", "a") + Expect(result).To(Equal("Amazon Elastic Compute Cloud\nJustification in narrative form A for AC-2\n")) + }) + }) +}) diff --git a/opencontrols/opencontrols_suite_test.go b/opencontrols/opencontrols_suite_test.go new file mode 100644 index 0000000..7acef92 --- /dev/null +++ b/opencontrols/opencontrols_suite_test.go @@ -0,0 +1,13 @@ +package opencontrols_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestOpencontrols(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Opencontrols Suite") +} From fe77edd389894d61b6260a56c4432ee2f13c3542 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sat, 27 Aug 2016 21:44:35 -0400 Subject: [PATCH 07/18] fix narrative tests --- opencontrols/data_test.go | 21 +++------------------ ssp/document_test.go | 2 +- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/opencontrols/data_test.go b/opencontrols/data_test.go index 83ffa3a..4acaf7b 100644 --- a/opencontrols/data_test.go +++ b/opencontrols/data_test.go @@ -1,37 +1,22 @@ package opencontrols_test import ( - "path/filepath" - - . "github.com/opencontrol/fedramp-templater/opencontrols" + "github.com/opencontrol/fedramp-templater/fixtures" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) -// TODO share with templater_test -func loadOpenControlFixture() Data { - openControlDir := filepath.Join("..", "fixtures", "opencontrols") - openControlDir, err := filepath.Abs(openControlDir) - Expect(err).NotTo(HaveOccurred()) - openControlData, errors := LoadFrom(openControlDir) - for _, err := range errors { - Expect(err).NotTo(HaveOccurred()) - } - - return openControlData -} - var _ = Describe("Data", func() { Describe("GetNarrative", func() { It("returns the relevant singular narrative", func() { - data := loadOpenControlFixture() + data := fixtures.LoadOpenControlFixture() result := data.GetNarrative("AC-2 (1)", "") Expect(result).To(Equal("Amazon Elastic Compute Cloud\nJustification in narrative form for AC-2 (1)\n")) }) It("returns the relevant narrative section", func() { - data := loadOpenControlFixture() + data := fixtures.LoadOpenControlFixture() result := data.GetNarrative("AC-2", "a") Expect(result).To(Equal("Amazon Elastic Compute Cloud\nJustification in narrative form A for AC-2\n")) }) diff --git a/ssp/document_test.go b/ssp/document_test.go index bac8b35..c57f380 100644 --- a/ssp/document_test.go +++ b/ssp/document_test.go @@ -37,7 +37,7 @@ var _ = Describe("SSP", func() { Describe("NarrativeTables", func() { It("returns the tables", func() { - doc := loadSSP("FedRAMP_ac-2_v2.1.docx") + doc := fixtures.LoadSSP("FedRAMP_ac-2_v2.1.docx") defer doc.Close() tables, err := doc.NarrativeTables() From e961533c216124bcc3884ad8cf361eeea8abeb24 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sat, 27 Aug 2016 22:20:34 -0400 Subject: [PATCH 08/18] refactor table struct out from SummaryTable --- control/responsible_role.go | 2 +- control/summary_table.go | 45 ++++------------------------- control/summary_table_test.go | 6 ++-- control/table.go | 53 +++++++++++++++++++++++++++++++++++ templater/templater.go | 4 +-- 5 files changed, 64 insertions(+), 46 deletions(-) create mode 100644 control/table.go diff --git a/control/responsible_role.go b/control/responsible_role.go index f9acb51..9c59cc3 100644 --- a/control/responsible_role.go +++ b/control/responsible_role.go @@ -11,7 +11,7 @@ import ( // findResponsibleRole looks for the Responsible Role cell in the control table. func findResponsibleRole(st *SummaryTable) (*responsibleRole, error) { - nodes, err := st.searchSubtree(".//w:tc[starts-with(normalize-space(.), 'Responsible Role')]") + nodes, err := st.tbl.searchSubtree(".//w:tc[starts-with(normalize-space(.), 'Responsible Role')]") if err != nil { return nil, err } diff --git a/control/summary_table.go b/control/summary_table.go index 4f853e9..bfbabc8 100644 --- a/control/summary_table.go +++ b/control/summary_table.go @@ -1,10 +1,6 @@ package control import ( - "errors" - "regexp" - "strings" - "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/fedramp-templater/opencontrols" "github.com/opencontrol/fedramp-templater/reporter" @@ -16,47 +12,16 @@ const ( // SummaryTable represents the node in the Word docx XML tree that corresponds to a security control. type SummaryTable struct { - Root xml.Node + tbl table } -func (st *SummaryTable) searchSubtree(xpath string) (nodes []xml.Node, err error) { - // http://stackoverflow.com/a/25387687/358804 - if !strings.HasPrefix(xpath, ".") { - err = errors.New("XPath must have leading period (`.`) to only search the subtree") - return - } - - return st.Root.Search(xpath) -} - -func (st *SummaryTable) tableHeader() (content string, err error) { - nodes, err := st.searchSubtree(".//w:tr") - if err != nil { - return - } - if len(nodes) == 0 { - err = errors.New("could not find control name") - return - } - // we only care about the first match - content = nodes[0].Content() - - return +func NewSummaryTable(root xml.Node) SummaryTable { + tbl := table{Root: root} + return SummaryTable{tbl} } func (st *SummaryTable) controlName() (name string, err error) { - content, err := st.tableHeader() - if err != nil { - return - } - - // matches controls and control enhancements, e.g. `AC-2`, `AC-2 (1)`, etc. - regex := regexp.MustCompile(`[A-Z]{2}-\d+( +\(\d+\))?`) - name = regex.FindString(content) - if name == "" { - err = errors.New("control name not found") - } - return + return st.tbl.controlName() } // Fill inserts the OpenControl justifications into the table. Note this modifies the `table`. diff --git a/control/summary_table_test.go b/control/summary_table_test.go index 9b5a3e6..edfdf32 100644 --- a/control/summary_table_test.go +++ b/control/summary_table_test.go @@ -46,7 +46,7 @@ var _ = Describe("SummaryTable", func() { Describe("Fill", func() { It("fills in the Responsible Role for controls", func() { table := getTable("AC-2") - st := SummaryTable{Root: table} + st := NewSummaryTable(table) openControlData := fixtures.LoadOpenControlFixture() st.Fill(openControlData) @@ -56,7 +56,7 @@ var _ = Describe("SummaryTable", func() { It("fills in the Responsible Role for control enhancements", func() { table := getTable("AC-2 (1)") - st := SummaryTable{Root: table} + st := NewSummaryTable(table) openControlData := fixtures.LoadOpenControlFixture() st.Fill(openControlData) @@ -68,7 +68,7 @@ var _ = Describe("SummaryTable", func() { Describe("Diff", func() { It("detects no diff when the value of responsible role is empty", func() { table := getTable("AC-2") - st := SummaryTable{Root: table} + st := NewSummaryTable(table) openControlData := fixtures.LoadOpenControlFixture() diff, err := st.Diff(openControlData) diff --git a/control/table.go b/control/table.go new file mode 100644 index 0000000..2af2ff2 --- /dev/null +++ b/control/table.go @@ -0,0 +1,53 @@ +package control + +import ( + "errors" + "regexp" + "strings" + + "github.com/jbowtie/gokogiri/xml" +) + +type table struct { + Root xml.Node +} + +func (t *table) searchSubtree(xpath string) (nodes []xml.Node, err error) { + // http://stackoverflow.com/a/25387687/358804 + if !strings.HasPrefix(xpath, ".") { + err = errors.New("XPath must have leading period (`.`) to only search the subtree") + return + } + + return t.Root.Search(xpath) +} + +func (t *table) tableHeader() (content string, err error) { + nodes, err := t.searchSubtree(".//w:tr") + if err != nil { + return + } + if len(nodes) == 0 { + err = errors.New("could not find control name") + return + } + // we only care about the first match + content = nodes[0].Content() + + return +} + +func (t *table) controlName() (name string, err error) { + content, err := t.tableHeader() + if err != nil { + return + } + + // matches controls and control enhancements, e.g. `AC-2`, `AC-2 (1)`, etc. + regex := regexp.MustCompile(`[A-Z]{2}-\d+( +\(\d+\))?`) + name = regex.FindString(content) + if name == "" { + err = errors.New("control name not found") + } + return +} diff --git a/templater/templater.go b/templater/templater.go index 67e9b3e..f87c21d 100644 --- a/templater/templater.go +++ b/templater/templater.go @@ -14,7 +14,7 @@ func TemplatizeSSP(s *ssp.Document, openControlData opencontrols.Data) (err erro return } for _, table := range tables { - st := control.SummaryTable{Root: table} + st := control.NewSummaryTable(table) err = st.Fill(openControlData) if err != nil { return err @@ -34,7 +34,7 @@ func DiffSSP(s *ssp.Document, openControlData opencontrols.Data) ([]reporter.Rep return diffInfo, err } for _, table := range tables { - st := control.SummaryTable{Root: table} + st := control.NewSummaryTable(table) tableDiffInfo, err := st.Diff(openControlData) if err != nil { return diffInfo, err From 56c1473eea252081486c95d7cef6dd886f1a5fef Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sat, 27 Aug 2016 22:24:57 -0400 Subject: [PATCH 09/18] create a NarrativeTable struct (broken) --- control/narrative_table.go | 38 +++++++++++++++++++++++++++++++++ control/narrative_table_test.go | 37 ++++++++++++++++++++++++++++++++ ssp/document.go | 28 ++++++++++++++++++++---- templater/templater.go | 28 +++++++++++++++++++++--- templater/templater_test.go | 13 +++++++++++ 5 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 control/narrative_table.go create mode 100644 control/narrative_table_test.go diff --git a/control/narrative_table.go b/control/narrative_table.go new file mode 100644 index 0000000..a66c773 --- /dev/null +++ b/control/narrative_table.go @@ -0,0 +1,38 @@ +package control + +import ( + "fmt" + + "github.com/jbowtie/gokogiri/xml" + "github.com/opencontrol/fedramp-templater/opencontrols" +) + +type NarrativeTable struct { + tbl table +} + +func NewNarrativeTable(root xml.Node) NarrativeTable { + tbl := table{Root: root} + return NarrativeTable{tbl} +} + +func (t *NarrativeTable) SectionRows() ([]xml.Node, error) { + // skip the header row + return t.tbl.searchSubtree(`.//w:tr[position() > 1]`) +} + +func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { + control, err := t.tbl.controlName() + if err != nil { + return + } + // TODO remove hard-coding + sectionKey := "b" + + narrative := openControlData.GetNarrative(control, sectionKey) + fmt.Println(narrative) + + // TODO fill it in + + return +} diff --git a/control/narrative_table_test.go b/control/narrative_table_test.go new file mode 100644 index 0000000..315278d --- /dev/null +++ b/control/narrative_table_test.go @@ -0,0 +1,37 @@ +package control_test + +import ( + . "github.com/opencontrol/fedramp-templater/control" + "github.com/opencontrol/fedramp-templater/fixtures" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("NarrativeTable", func() { + Describe("SectionRows", func() { + It("returns the correct number for a singular narrative", func() { + doc := fixtures.LoadSSP("FedRAMP_ac-2-1_v2.1.docx") + defer doc.Close() + root, err := doc.NarrativeTable("AC-2 (1)") + Expect(err).NotTo(HaveOccurred()) + + table := NewNarrativeTable(root) + sections, err := table.SectionRows() + Expect(err).NotTo(HaveOccurred()) + Expect(len(sections)).To(Equal(1)) + }) + + It("returns the correct number for multiple narrative sections", func() { + doc := fixtures.LoadSSP("FedRAMP_ac-2_v2.1.docx") + defer doc.Close() + root, err := doc.NarrativeTable("AC-2") + Expect(err).NotTo(HaveOccurred()) + + table := NewNarrativeTable(root) + sections, err := table.SectionRows() + Expect(err).NotTo(HaveOccurred()) + Expect(len(sections)).To(Equal(11)) + }) + }) +}) diff --git a/ssp/document.go b/ssp/document.go index 6962936..c4f43c2 100644 --- a/ssp/document.go +++ b/ssp/document.go @@ -1,6 +1,8 @@ package ssp import ( + "fmt" + "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/doc-template/docx" "github.com/opencontrol/fedramp-templater/docx/helper" @@ -37,15 +39,33 @@ func Load(path string) (ssp *Document, err error) { } // SummaryTables returns the summary tables for the controls and the control enhancements. -func (s *Document) SummaryTables() (tables []xml.Node, err error) { +func (s *Document) SummaryTables() ([]xml.Node, error) { // find the tables matching the provided headers, ignoring whitespace return s.xmlDoc.Search(SummaryTablesXPath) } -// NarrativeTables returns the narrative tables for the controls and the control enhancements. -func (s *Document) NarrativeTables() (tables []xml.Node, err error) { +// to retrieve all narrative tables, pass in an empty string +func (s *Document) findNarrativeTables(control string) ([]xml.Node, error) { // find the tables matching the provided headers, ignoring whitespace - return s.xmlDoc.Search("//w:tbl[contains(normalize-space(.), 'What is the solution and how is it implemented?')]") + xpath := fmt.Sprintf("//w:tbl[contains(normalize-space(.), '%s What is the solution and how is it implemented?')]", control) + return s.xmlDoc.Search(xpath) + // TODO return NarrativeTables +} + +// NarrativeTables returns the narrative tables for the controls and the control enhancements. +func (s *Document) NarrativeTables() ([]xml.Node, error) { + return s.findNarrativeTables("") +} + +// NarrativeTables returns the narrative tables for the specified control or control enhancement. +func (s *Document) NarrativeTable(control string) (table xml.Node, err error) { + tables, err := s.findNarrativeTables(control) + if err != nil { + return + } + // TODO return error if there is more than one + table = tables[0] + return } // Content retrieves the text from within the Word document. diff --git a/templater/templater.go b/templater/templater.go index f87c21d..8c146a5 100644 --- a/templater/templater.go +++ b/templater/templater.go @@ -7,8 +7,7 @@ import ( "github.com/opencontrol/fedramp-templater/ssp" ) -// TemplatizeSSP inserts OpenControl data into (i.e. modifies) the provided SSP. -func TemplatizeSSP(s *ssp.Document, openControlData opencontrols.Data) (err error) { +func fillSummaryTables(s *ssp.Document, openControlData opencontrols.Data) (err error) { tables, err := s.SummaryTables() if err != nil { return @@ -17,10 +16,33 @@ func TemplatizeSSP(s *ssp.Document, openControlData opencontrols.Data) (err erro st := control.NewSummaryTable(table) err = st.Fill(openControlData) if err != nil { - return err + return + } + } + + return +} + +func fillNarrativeTables(s *ssp.Document, openControlData opencontrols.Data) (err error) { + tables, err := s.NarrativeTables() + if err != nil { + return + } + for _, table := range tables { + ct := control.NewNarrativeTable(table) + err = ct.Fill(openControlData) + if err != nil { + return } } + return +} + +// TemplatizeSSP inserts OpenControl data into (i.e. modifies) the provided SSP. +func TemplatizeSSP(s *ssp.Document, openControlData opencontrols.Data) (err error) { + fillSummaryTables(s, openControlData) + fillNarrativeTables(s, openControlData) s.UpdateContent() return diff --git a/templater/templater_test.go b/templater/templater_test.go index e24393c..ced9493 100644 --- a/templater/templater_test.go +++ b/templater/templater_test.go @@ -32,6 +32,19 @@ var _ = Describe("Templater", func() { content := doc.Content() Expect(content).To(ContainSubstring(`Responsible Role: Amazon Elastic Compute Cloud: AWS Staff`)) }) + + It("fills in the narrative field", func() { + doc := fixtures.LoadSSP("FedRAMP_ac-2_v2.1.docx") + defer doc.Close() + openControlData := fixtures.LoadOpenControlFixture() + + err := TemplatizeSSP(doc, openControlData) + + Expect(err).NotTo(HaveOccurred()) + content := doc.Content() + Expect(content).To(ContainSubstring(`Justification in narrative form B for AC-2`)) + Expect(content).To(ContainSubstring(`Justification in narrative form for AC-2 (1)`)) + }) }) Describe("DiffSSP", func() { From 397aa02eca9091914f4c99a566126d50c1265046 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sat, 27 Aug 2016 23:21:52 -0400 Subject: [PATCH 10/18] fill narrative tables --- control/narrative_table.go | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/control/narrative_table.go b/control/narrative_table.go index a66c773..6eb33b0 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -1,8 +1,6 @@ package control import ( - "fmt" - "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/fedramp-templater/opencontrols" ) @@ -26,13 +24,39 @@ func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { if err != nil { return } - // TODO remove hard-coding - sectionKey := "b" - narrative := openControlData.GetNarrative(control, sectionKey) - fmt.Println(narrative) + rows, err := t.SectionRows() + if err != nil { + return + } - // TODO fill it in + if len(rows) == 1 { + // singular narrative + row := rows[0] + textFields, err := row.Search(`(./w:tc/w:p)[1]`) + if err != nil { + return err + } + textField := textFields[0] + + narrative := openControlData.GetNarrative(control, "") + textField.SetContent(narrative) + } else { + // multiple parts + for _, row := range rows { + // TODO remove hard-coding + sectionKey := "b" + + textFields, err := row.Search(`./w:tc[position() = 1]/w:p`) + if err != nil { + return err + } + textField := textFields[0] + + narrative := openControlData.GetNarrative(control, sectionKey) + textField.SetContent(narrative) + } + } return } From dcca032f170975be66e2ac974e25da5225b54acd Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sat, 27 Aug 2016 23:51:55 -0400 Subject: [PATCH 11/18] refactor narrative row filling --- control/narrative_table.go | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/control/narrative_table.go b/control/narrative_table.go index 6eb33b0..e99f5dc 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -5,6 +5,13 @@ import ( "github.com/opencontrol/fedramp-templater/opencontrols" ) +func fillRow(row xml.Node, data opencontrols.Data, control string, section string) { + // equivalent XPath: `./w:tc[last()]/w:p[1]` + textField := row.LastChild().FirstChild() + narrative := data.GetNarrative(control, section) + textField.SetContent(narrative) +} + type NarrativeTable struct { tbl table } @@ -33,28 +40,13 @@ func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { if len(rows) == 1 { // singular narrative row := rows[0] - textFields, err := row.Search(`(./w:tc/w:p)[1]`) - if err != nil { - return err - } - textField := textFields[0] - - narrative := openControlData.GetNarrative(control, "") - textField.SetContent(narrative) + fillRow(row, openControlData, control, "") } else { // multiple parts for _, row := range rows { // TODO remove hard-coding sectionKey := "b" - - textFields, err := row.Search(`./w:tc[position() = 1]/w:p`) - if err != nil { - return err - } - textField := textFields[0] - - narrative := openControlData.GetNarrative(control, sectionKey) - textField.SetContent(narrative) + fillRow(row, openControlData, control, sectionKey) } } From 21a79d201500d5ea9dde75514a6aef159a40e6ed Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sun, 28 Aug 2016 01:03:17 -0400 Subject: [PATCH 12/18] fix narrative row filling --- control/narrative_table.go | 45 ++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/control/narrative_table.go b/control/narrative_table.go index e99f5dc..29bd082 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -1,15 +1,40 @@ package control import ( + "errors" + "regexp" + "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/fedramp-templater/opencontrols" ) -func fillRow(row xml.Node, data opencontrols.Data, control string, section string) { - // equivalent XPath: `./w:tc[last()]/w:p[1]` - textField := row.LastChild().FirstChild() +func findSectionKey(row xml.Node) (section string, err error) { + re := regexp.MustCompile(`Part ([a-z])`) + subMatches := re.FindSubmatch([]byte(row.Content())) + if len(subMatches) != 2 { + err = errors.New("No Parts found.") + return + } + section = string(subMatches[1]) + return +} + +func fillRow(row xml.Node, data opencontrols.Data, control string, section string) (err error) { + paragraphNodes, err := row.Search(`./w:tc[last()]/w:p[1]`) + if err != nil { + return + } + paragraphNode := paragraphNodes[0] + + err = paragraphNode.SetChildren(``) + if err != nil { + return + } + textCell := paragraphNode.FirstChild().FirstChild() + narrative := data.GetNarrative(control, section) - textField.SetContent(narrative) + textCell.SetContent(narrative) + return } type NarrativeTable struct { @@ -44,9 +69,15 @@ func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { } else { // multiple parts for _, row := range rows { - // TODO remove hard-coding - sectionKey := "b" - fillRow(row, openControlData, control, sectionKey) + sectionKey, err := findSectionKey(row) + if err != nil { + return err + } + + err = fillRow(row, openControlData, control, sectionKey) + if err != nil { + return err + } } } From 72139b5434d3261438d71d44b46e6180ca99dac1 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sun, 28 Aug 2016 01:11:56 -0400 Subject: [PATCH 13/18] refactor NarrativeTable --- control/narrative_table.go | 38 ++++++++++++++++++++------------------ docx/helper/helper.go | 12 ++++++++++++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/control/narrative_table.go b/control/narrative_table.go index 29bd082..25f61d4 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -5,6 +5,7 @@ import ( "regexp" "github.com/jbowtie/gokogiri/xml" + "github.com/opencontrol/fedramp-templater/docx/helper" "github.com/opencontrol/fedramp-templater/opencontrols" ) @@ -20,23 +21,34 @@ func findSectionKey(row xml.Node) (section string, err error) { } func fillRow(row xml.Node, data opencontrols.Data, control string, section string) (err error) { + // the row should have one or two cells; either way, the last one is what should be filled paragraphNodes, err := row.Search(`./w:tc[last()]/w:p[1]`) if err != nil { return } paragraphNode := paragraphNodes[0] - err = paragraphNode.SetChildren(``) - if err != nil { - return - } - textCell := paragraphNode.FirstChild().FirstChild() - narrative := data.GetNarrative(control, section) - textCell.SetContent(narrative) + helper.FillParagraph(paragraphNode, narrative) + return } +func fillRows(rows []xml.Node, data opencontrols.Data, control string) error { + for _, row := range rows { + sectionKey, err := findSectionKey(row) + if err != nil { + return err + } + + err = fillRow(row, data, control, sectionKey) + if err != nil { + return err + } + } + return nil +} + type NarrativeTable struct { tbl table } @@ -68,17 +80,7 @@ func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { fillRow(row, openControlData, control, "") } else { // multiple parts - for _, row := range rows { - sectionKey, err := findSectionKey(row) - if err != nil { - return err - } - - err = fillRow(row, openControlData, control, sectionKey) - if err != nil { - return err - } - } + fillRows(rows, openControlData, control) } return diff --git a/docx/helper/helper.go b/docx/helper/helper.go index 138c68a..e76896a 100644 --- a/docx/helper/helper.go +++ b/docx/helper/helper.go @@ -25,3 +25,15 @@ func GenerateXML(wordDoc *docx.Docx) (xmlDoc *xml.XmlDocument, err error) { bytes := []byte(content) return ParseXML(bytes) } + +func FillParagraph(paragraph xml.Node, content string) (err error) { + // this seems to be the easiest way to create child notes + err = paragraph.SetChildren(``) + if err != nil { + return + } + textCell := paragraph.FirstChild().FirstChild() + + textCell.SetContent(content) + return +} From 55db87ac1b99034a5743c1f50d04f670dddabacb Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sun, 28 Aug 2016 01:30:44 -0400 Subject: [PATCH 14/18] fix Code Climate errors --- control/narrative_table.go | 4 ++++ control/summary_table.go | 3 ++- docx/helper/helper.go | 1 + ssp/document.go | 14 ++++++++++---- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/control/narrative_table.go b/control/narrative_table.go index 25f61d4..3ee5e12 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -49,20 +49,24 @@ func fillRows(rows []xml.Node, data opencontrols.Data, control string) error { return nil } +// NarrativeTable represents the node in the Word docx XML tree that corresponds to the justification fields for a security control. type NarrativeTable struct { tbl table } +// NewNarrativeTable creates a NarrativeTable instance. func NewNarrativeTable(root xml.Node) NarrativeTable { tbl := table{Root: root} return NarrativeTable{tbl} } +// SectionRows returns the list of rows which correspond to each "part" of the narrative. Will return a single row when the narrative isn't split into parts. func (t *NarrativeTable) SectionRows() ([]xml.Node, error) { // skip the header row return t.tbl.searchSubtree(`.//w:tr[position() > 1]`) } +// Fill inserts the OpenControl data into the table. func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { control, err := t.tbl.controlName() if err != nil { diff --git a/control/summary_table.go b/control/summary_table.go index bfbabc8..a9c8941 100644 --- a/control/summary_table.go +++ b/control/summary_table.go @@ -10,11 +10,12 @@ const ( responsibleRoleField = "Responsible Role" ) -// SummaryTable represents the node in the Word docx XML tree that corresponds to a security control. +// SummaryTable represents the node in the Word docx XML tree that corresponds to the summary information for a security control. type SummaryTable struct { tbl table } +// NewSummaryTable creates a SummaryTable instance. func NewSummaryTable(root xml.Node) SummaryTable { tbl := table{Root: root} return SummaryTable{tbl} diff --git a/docx/helper/helper.go b/docx/helper/helper.go index e76896a..ee7fd50 100644 --- a/docx/helper/helper.go +++ b/docx/helper/helper.go @@ -26,6 +26,7 @@ func GenerateXML(wordDoc *docx.Docx) (xmlDoc *xml.XmlDocument, err error) { return ParseXML(bytes) } +// FillParagraph inserts the given content into the provided docx XML paragraph node. func FillParagraph(paragraph xml.Node, content string) (err error) { // this seems to be the easiest way to create child notes err = paragraph.SetChildren(``) diff --git a/ssp/document.go b/ssp/document.go index c4f43c2..0f97365 100644 --- a/ssp/document.go +++ b/ssp/document.go @@ -1,6 +1,7 @@ package ssp import ( + "errors" "fmt" "github.com/jbowtie/gokogiri/xml" @@ -49,21 +50,26 @@ func (s *Document) findNarrativeTables(control string) ([]xml.Node, error) { // find the tables matching the provided headers, ignoring whitespace xpath := fmt.Sprintf("//w:tbl[contains(normalize-space(.), '%s What is the solution and how is it implemented?')]", control) return s.xmlDoc.Search(xpath) - // TODO return NarrativeTables } -// NarrativeTables returns the narrative tables for the controls and the control enhancements. +// NarrativeTables returns the narrative tables for all controls and the control enhancements. func (s *Document) NarrativeTables() ([]xml.Node, error) { return s.findNarrativeTables("") } -// NarrativeTables returns the narrative tables for the specified control or control enhancement. +// NarrativeTable returns the narrative table for the specified control or control enhancement. func (s *Document) NarrativeTable(control string) (table xml.Node, err error) { tables, err := s.findNarrativeTables(control) if err != nil { return } - // TODO return error if there is more than one + if len(tables) == 0 { + err = errors.New("No narrative tables found.") + return + } else if len(tables) > 1 { + err = errors.New("Too many narrative tables were matched.") + return + } table = tables[0] return } From 3234e7ac9f4a0caf8e0e59835d01b06d45be9dce Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sun, 28 Aug 2016 02:15:30 -0400 Subject: [PATCH 15/18] respect newlines in narrative content --- control/narrative_table.go | 6 +++--- docx/helper/helper.go | 31 ++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/control/narrative_table.go b/control/narrative_table.go index 3ee5e12..d6b6c53 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -22,14 +22,14 @@ func findSectionKey(row xml.Node) (section string, err error) { func fillRow(row xml.Node, data opencontrols.Data, control string, section string) (err error) { // the row should have one or two cells; either way, the last one is what should be filled - paragraphNodes, err := row.Search(`./w:tc[last()]/w:p[1]`) + cellNodes, err := row.Search(`./w:tc[last()]`) if err != nil { return } - paragraphNode := paragraphNodes[0] + cellNode := cellNodes[0] narrative := data.GetNarrative(control, section) - helper.FillParagraph(paragraphNode, narrative) + helper.FillCell(cellNode, narrative) return } diff --git a/docx/helper/helper.go b/docx/helper/helper.go index ee7fd50..4d899d2 100644 --- a/docx/helper/helper.go +++ b/docx/helper/helper.go @@ -1,6 +1,8 @@ package helper import ( + "strings" + "github.com/jbowtie/gokogiri" "github.com/jbowtie/gokogiri/xml" "github.com/opencontrol/doc-template/docx" @@ -26,7 +28,7 @@ func GenerateXML(wordDoc *docx.Docx) (xmlDoc *xml.XmlDocument, err error) { return ParseXML(bytes) } -// FillParagraph inserts the given content into the provided docx XML paragraph node. +// FillParagraph inserts the given content into the provided docx XML paragraph node. Note that newlines aren't respected - you'll need to create a new paragraph node for each. func FillParagraph(paragraph xml.Node, content string) (err error) { // this seems to be the easiest way to create child notes err = paragraph.SetChildren(``) @@ -38,3 +40,30 @@ func FillParagraph(paragraph xml.Node, content string) (err error) { textCell.SetContent(content) return } + +// AddMultiLineContent adds the given content into the provided docx XML node as multiple "paragraphs" of text, split by the newlines. +func AddMultiLineContent(parent xml.Node, content string) error { + lines := strings.Split(content, "\n") + for _, line := range lines { + // this seems to be the easiest way to create child notes + err := parent.AddChild(``) + if err != nil { + return err + } + paragraph := parent.LastChild() + FillParagraph(paragraph, line) + } + + return nil +} + +// FillCell inserts the given content into the provided docx XML table cell node. +func FillCell(cell xml.Node, content string) error { + // clear out the existing content/structure + err := cell.SetChildren("") + if err != nil { + return err + } + + return AddMultiLineContent(cell, content) +} From 06655b795702db124213cb63b00995724ddada34 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sun, 28 Aug 2016 12:02:07 -0400 Subject: [PATCH 16/18] embed tables directly in the Narrative/SummaryTable structs --- control/narrative_table.go | 6 +++--- control/responsible_role.go | 4 ++-- control/summary_table.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/control/narrative_table.go b/control/narrative_table.go index d6b6c53..54513a5 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -51,7 +51,7 @@ func fillRows(rows []xml.Node, data opencontrols.Data, control string) error { // NarrativeTable represents the node in the Word docx XML tree that corresponds to the justification fields for a security control. type NarrativeTable struct { - tbl table + table } // NewNarrativeTable creates a NarrativeTable instance. @@ -63,12 +63,12 @@ func NewNarrativeTable(root xml.Node) NarrativeTable { // SectionRows returns the list of rows which correspond to each "part" of the narrative. Will return a single row when the narrative isn't split into parts. func (t *NarrativeTable) SectionRows() ([]xml.Node, error) { // skip the header row - return t.tbl.searchSubtree(`.//w:tr[position() > 1]`) + return t.table.searchSubtree(`.//w:tr[position() > 1]`) } // Fill inserts the OpenControl data into the table. func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { - control, err := t.tbl.controlName() + control, err := t.table.controlName() if err != nil { return } diff --git a/control/responsible_role.go b/control/responsible_role.go index 9c59cc3..879ce2c 100644 --- a/control/responsible_role.go +++ b/control/responsible_role.go @@ -10,8 +10,8 @@ import ( ) // findResponsibleRole looks for the Responsible Role cell in the control table. -func findResponsibleRole(st *SummaryTable) (*responsibleRole, error) { - nodes, err := st.tbl.searchSubtree(".//w:tc[starts-with(normalize-space(.), 'Responsible Role')]") +func findResponsibleRole(ct *SummaryTable) (*responsibleRole, error) { + nodes, err := ct.table.searchSubtree(".//w:tc[starts-with(normalize-space(.), 'Responsible Role')]") if err != nil { return nil, err } diff --git a/control/summary_table.go b/control/summary_table.go index a9c8941..2076cee 100644 --- a/control/summary_table.go +++ b/control/summary_table.go @@ -12,7 +12,7 @@ const ( // SummaryTable represents the node in the Word docx XML tree that corresponds to the summary information for a security control. type SummaryTable struct { - tbl table + table } // NewSummaryTable creates a SummaryTable instance. @@ -22,7 +22,7 @@ func NewSummaryTable(root xml.Node) SummaryTable { } func (st *SummaryTable) controlName() (name string, err error) { - return st.tbl.controlName() + return st.table.controlName() } // Fill inserts the OpenControl justifications into the table. Note this modifies the `table`. From ccae62663b8fd34602ba3efed7485d4deeee3672 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sun, 28 Aug 2016 12:28:25 -0400 Subject: [PATCH 17/18] create a narrativeSection struct --- control/narrative_section.go | 65 ++++++++++++++++++++++++++++++++++++ control/narrative_table.go | 47 ++------------------------ 2 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 control/narrative_section.go diff --git a/control/narrative_section.go b/control/narrative_section.go new file mode 100644 index 0000000..f0ce484 --- /dev/null +++ b/control/narrative_section.go @@ -0,0 +1,65 @@ +package control + +import ( + "errors" + "regexp" + + "github.com/jbowtie/gokogiri/xml" + "github.com/opencontrol/fedramp-templater/docx/helper" + "github.com/opencontrol/fedramp-templater/opencontrols" +) + +type narrativeSection struct { + row xml.Node +} + +func (n narrativeSection) parsePart() (key string, err error) { + re := regexp.MustCompile(`Part ([a-z])`) + content := []byte(n.row.Content()) + subMatches := re.FindSubmatch(content) + if len(subMatches) != 2 { + err = errors.New("No Parts found.") + return + } + key = string(subMatches[1]) + return +} + +// GetKey returns the narrative section "part"/key. `key` will be an empty string if there is no "Part". +func (n narrativeSection) GetKey() (key string, err error) { + cells, err := n.row.Search(`./w:tc`) + numCells := len(cells) + if numCells == 1 { + // there is only a single narrative section + key = "" + } else if numCells == 2 { + key, err = n.parsePart() + if err != nil { + return + } + } else { + err = errors.New("Don't know how to parse row.") + } + + return +} + +// Fill populates the section/part with the narrative for this control part from the provided data. +func (n narrativeSection) Fill(data opencontrols.Data, control string) (err error) { + // the row should have one or two cells; either way, the last one is what should be filled + cellNodes, err := n.row.Search(`./w:tc[last()]`) + if err != nil { + return + } + cellNode := cellNodes[0] + + key, err := n.GetKey() + if err != nil { + return + } + + narrative := data.GetNarrative(control, key) + helper.FillCell(cellNode, narrative) + + return +} diff --git a/control/narrative_table.go b/control/narrative_table.go index 54513a5..32bc37c 100644 --- a/control/narrative_table.go +++ b/control/narrative_table.go @@ -1,47 +1,14 @@ package control import ( - "errors" - "regexp" - "github.com/jbowtie/gokogiri/xml" - "github.com/opencontrol/fedramp-templater/docx/helper" "github.com/opencontrol/fedramp-templater/opencontrols" ) -func findSectionKey(row xml.Node) (section string, err error) { - re := regexp.MustCompile(`Part ([a-z])`) - subMatches := re.FindSubmatch([]byte(row.Content())) - if len(subMatches) != 2 { - err = errors.New("No Parts found.") - return - } - section = string(subMatches[1]) - return -} - -func fillRow(row xml.Node, data opencontrols.Data, control string, section string) (err error) { - // the row should have one or two cells; either way, the last one is what should be filled - cellNodes, err := row.Search(`./w:tc[last()]`) - if err != nil { - return - } - cellNode := cellNodes[0] - - narrative := data.GetNarrative(control, section) - helper.FillCell(cellNode, narrative) - - return -} - func fillRows(rows []xml.Node, data opencontrols.Data, control string) error { for _, row := range rows { - sectionKey, err := findSectionKey(row) - if err != nil { - return err - } - - err = fillRow(row, data, control, sectionKey) + section := narrativeSection{row} + err := section.Fill(data, control) if err != nil { return err } @@ -78,14 +45,6 @@ func (t *NarrativeTable) Fill(openControlData opencontrols.Data) (err error) { return } - if len(rows) == 1 { - // singular narrative - row := rows[0] - fillRow(row, openControlData, control, "") - } else { - // multiple parts - fillRows(rows, openControlData, control) - } - + fillRows(rows, openControlData, control) return } From 74ea43516ced200d0ed6debf0e6ce0579e365907 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Sun, 28 Aug 2016 12:52:47 -0400 Subject: [PATCH 18/18] create an XML helper package --- control/narrative_section.go | 10 +++++----- control/responsible_role.go | 3 ++- control/table.go | 12 +++--------- xml/helper/helper.go | 28 ++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 xml/helper/helper.go diff --git a/control/narrative_section.go b/control/narrative_section.go index f0ce484..0937d96 100644 --- a/control/narrative_section.go +++ b/control/narrative_section.go @@ -5,8 +5,9 @@ import ( "regexp" "github.com/jbowtie/gokogiri/xml" - "github.com/opencontrol/fedramp-templater/docx/helper" + docxHelper "github.com/opencontrol/fedramp-templater/docx/helper" "github.com/opencontrol/fedramp-templater/opencontrols" + xmlHelper "github.com/opencontrol/fedramp-templater/xml/helper" ) type narrativeSection struct { @@ -27,7 +28,7 @@ func (n narrativeSection) parsePart() (key string, err error) { // GetKey returns the narrative section "part"/key. `key` will be an empty string if there is no "Part". func (n narrativeSection) GetKey() (key string, err error) { - cells, err := n.row.Search(`./w:tc`) + cells, err := xmlHelper.SearchSubtree(n.row, `./w:tc`) numCells := len(cells) if numCells == 1 { // there is only a single narrative section @@ -47,11 +48,10 @@ func (n narrativeSection) GetKey() (key string, err error) { // Fill populates the section/part with the narrative for this control part from the provided data. func (n narrativeSection) Fill(data opencontrols.Data, control string) (err error) { // the row should have one or two cells; either way, the last one is what should be filled - cellNodes, err := n.row.Search(`./w:tc[last()]`) + cellNode, err := xmlHelper.SearchOne(n.row, `./w:tc[last()]`) if err != nil { return } - cellNode := cellNodes[0] key, err := n.GetKey() if err != nil { @@ -59,7 +59,7 @@ func (n narrativeSection) Fill(data opencontrols.Data, control string) (err erro } narrative := data.GetNarrative(control, key) - helper.FillCell(cellNode, narrative) + docxHelper.FillCell(cellNode, narrative) return } diff --git a/control/responsible_role.go b/control/responsible_role.go index 879ce2c..60de5c4 100644 --- a/control/responsible_role.go +++ b/control/responsible_role.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/jbowtie/gokogiri/xml" + "github.com/opencontrol/fedramp-templater/xml/helper" ) // findResponsibleRole looks for the Responsible Role cell in the control table. @@ -19,7 +20,7 @@ func findResponsibleRole(ct *SummaryTable) (*responsibleRole, error) { return nil, errors.New("could not find Responsible Role cell") } parentNode := nodes[0] - childNodes, err := parentNode.Search(".//w:t") + childNodes, err := helper.SearchSubtree(parentNode, `.//w:t`) if err != nil || len(childNodes) < 1 { return nil, errors.New("Should not happen, cannot find text nodes.") } diff --git a/control/table.go b/control/table.go index 2af2ff2..be1a3c2 100644 --- a/control/table.go +++ b/control/table.go @@ -3,23 +3,17 @@ package control import ( "errors" "regexp" - "strings" "github.com/jbowtie/gokogiri/xml" + "github.com/opencontrol/fedramp-templater/xml/helper" ) type table struct { Root xml.Node } -func (t *table) searchSubtree(xpath string) (nodes []xml.Node, err error) { - // http://stackoverflow.com/a/25387687/358804 - if !strings.HasPrefix(xpath, ".") { - err = errors.New("XPath must have leading period (`.`) to only search the subtree") - return - } - - return t.Root.Search(xpath) +func (t *table) searchSubtree(xpath string) ([]xml.Node, error) { + return helper.SearchSubtree(t.Root, xpath) } func (t *table) tableHeader() (content string, err error) { diff --git a/xml/helper/helper.go b/xml/helper/helper.go new file mode 100644 index 0000000..ba03eae --- /dev/null +++ b/xml/helper/helper.go @@ -0,0 +1,28 @@ +package helper + +import ( + "errors" + "strings" + + "github.com/jbowtie/gokogiri/xml" +) + +// SearchSubtree searches the subtree of the given root node. +func SearchSubtree(root xml.Node, xpath string) (nodes []xml.Node, err error) { + // http://stackoverflow.com/a/25387687/358804 + if !strings.HasPrefix(xpath, ".") { + err = errors.New("XPath must have leading period (`.`) to only search the subtree") + return + } + + return root.Search(xpath) +} + +// SearchSubtree searches the subtree of the given root node and returns the first result. +func SearchOne(root xml.Node, xpath string) (xml.Node, error) { + results, err := SearchSubtree(root, xpath) + if err != nil { + return nil, err + } + return results[0], nil +}