Refactor SOA generation
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
56
pkg/soagen/applicability.go
Normal file
56
pkg/soagen/applicability.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
import "github.com/xuri/excelize/v2"
|
||||
|
||||
type Applicability struct {
|
||||
Value string
|
||||
IsApplicable bool
|
||||
}
|
||||
|
||||
func NewApplicability(value string, isApplicable bool) Applicability {
|
||||
return Applicability{
|
||||
Value: value,
|
||||
IsApplicable: isApplicable,
|
||||
}
|
||||
}
|
||||
|
||||
func (a Applicability) String() string {
|
||||
return a.Value
|
||||
}
|
||||
|
||||
func (a Applicability) MarshalExcel() ExcelValue {
|
||||
color := "#FFB6C1" // Light red for not applicable
|
||||
if a.IsApplicable {
|
||||
color = "#90EE90" // Light green for applicable
|
||||
}
|
||||
|
||||
return ExcelValue{
|
||||
Value: a.Value,
|
||||
Style: &excelize.Style{
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "#000000", Style: 1},
|
||||
{Type: "top", Color: "#000000", Style: 1},
|
||||
{Type: "bottom", Color: "#000000", Style: 1},
|
||||
{Type: "right", Color: "#000000", Style: 1},
|
||||
},
|
||||
Fill: excelize.Fill{Type: "pattern", Color: []string{color}, Pattern: 1},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
|
||||
},
|
||||
Validation: []string{"Yes", "No"},
|
||||
Width: 12,
|
||||
}
|
||||
}
|
||||
28
pkg/soagen/excel_value.go
Normal file
28
pkg/soagen/excel_value.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
import "github.com/xuri/excelize/v2"
|
||||
|
||||
type ExcelValue struct {
|
||||
Value interface{}
|
||||
Style *excelize.Style
|
||||
Validation []string
|
||||
Width float64
|
||||
}
|
||||
|
||||
type ExcelMarshaler interface {
|
||||
MarshalExcel() ExcelValue
|
||||
}
|
||||
101
pkg/soagen/field_config.go
Normal file
101
pkg/soagen/field_config.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
type FieldConfiguration struct {
|
||||
Field string
|
||||
Columns []string
|
||||
FilterColumns []string // Specific columns that should have filters (subset of Columns)
|
||||
Width []float64 // Width for each column
|
||||
DefaultWidth float64 // Default width if not specified
|
||||
HasFilter bool // Whether to enable auto-filter
|
||||
}
|
||||
|
||||
var (
|
||||
soaFieldConfigs = []FieldConfiguration{
|
||||
{
|
||||
Field: "SectionTitle",
|
||||
Columns: []string{"A"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{12},
|
||||
DefaultWidth: 12,
|
||||
HasFilter: false,
|
||||
},
|
||||
{
|
||||
Field: "ControlName",
|
||||
Columns: []string{"B"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{35},
|
||||
DefaultWidth: 35,
|
||||
HasFilter: false,
|
||||
},
|
||||
{
|
||||
Field: "Applicability",
|
||||
Columns: []string{"C"},
|
||||
FilterColumns: []string{"C"},
|
||||
Width: []float64{12},
|
||||
DefaultWidth: 12,
|
||||
HasFilter: true,
|
||||
},
|
||||
{
|
||||
Field: "JustificationExclusion",
|
||||
Columns: []string{"D"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{25},
|
||||
DefaultWidth: 25,
|
||||
HasFilter: false,
|
||||
},
|
||||
{
|
||||
Field: "Regulatory",
|
||||
Columns: []string{"E"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{12},
|
||||
DefaultWidth: 12,
|
||||
HasFilter: false,
|
||||
},
|
||||
{
|
||||
Field: "Contractual",
|
||||
Columns: []string{"F"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{12},
|
||||
DefaultWidth: 12,
|
||||
HasFilter: false,
|
||||
},
|
||||
{
|
||||
Field: "BestPractice",
|
||||
Columns: []string{"G"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{12},
|
||||
DefaultWidth: 12,
|
||||
HasFilter: false,
|
||||
},
|
||||
{
|
||||
Field: "RiskAssessment",
|
||||
Columns: []string{"H"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{12},
|
||||
DefaultWidth: 12,
|
||||
HasFilter: false,
|
||||
},
|
||||
{
|
||||
Field: "SecurityMeasures",
|
||||
Columns: []string{"I"},
|
||||
FilterColumns: []string{},
|
||||
Width: []float64{40},
|
||||
DefaultWidth: 40,
|
||||
HasFilter: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
301
pkg/soagen/generator.go
Normal file
301
pkg/soagen/generator.go
Normal file
@@ -0,0 +1,301 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func GenerateSOAExcel(data SOAData) ([]byte, error) {
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
sheetName := "State of Applicability"
|
||||
f.NewSheet(sheetName)
|
||||
f.DeleteSheet("Sheet1")
|
||||
|
||||
if err := setupSOAHeader(f, sheetName); err != nil {
|
||||
return nil, fmt.Errorf("cannot setup Excel header: %w", err)
|
||||
}
|
||||
|
||||
if err := populateSOAData(f, sheetName, data.Rows); err != nil {
|
||||
return nil, fmt.Errorf("cannot populate Excel data: %w", err)
|
||||
}
|
||||
|
||||
if err := applySOAFinalFormatting(f, sheetName, len(data.Rows)); err != nil {
|
||||
return nil, fmt.Errorf("cannot apply final formatting: %w", err)
|
||||
}
|
||||
|
||||
buf, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot write Excel file to buffer: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func setupSOAHeader(f *excelize.File, sheetName string) error {
|
||||
headerStyle, err := f.NewStyle(getHeaderStyle())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cellStyle, err := f.NewStyle(getCellStyle())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return applyHeaderLayout(f, sheetName, getSOAHeaderLayout(), headerStyle, cellStyle)
|
||||
}
|
||||
|
||||
func populateSOAData(f *excelize.File, sheetName string, rows []SOARowData) error {
|
||||
dataStartRow := 8
|
||||
|
||||
for i, rowData := range rows {
|
||||
isFirstRow := i == 0
|
||||
filterColumns, err := writeSOARow(f, sheetName, dataStartRow+i, rowData, isFirstRow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write row %d: %w", dataStartRow+i, err)
|
||||
}
|
||||
|
||||
if isFirstRow && len(filterColumns) > 0 {
|
||||
if err := applySOAAutoFilter(f, sheetName, filterColumns); err != nil {
|
||||
return fmt.Errorf("cannot apply auto filter: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeSOARow(f *excelize.File, sheetName string, row int, data SOARowData, isFirstRow bool) ([]string, error) {
|
||||
var allFilterColumns []string
|
||||
|
||||
fields := []struct {
|
||||
name string
|
||||
value interface{}
|
||||
}{
|
||||
{"SectionTitle", data.SectionTitle},
|
||||
{"ControlName", data.ControlName},
|
||||
{"Applicability", data.Applicability},
|
||||
{"Regulatory", data.Regulatory},
|
||||
{"Contractual", data.Contractual},
|
||||
{"BestPractice", data.BestPractice},
|
||||
{"RiskAssessment", data.RiskAssessment},
|
||||
{"JustificationExclusion", data.JustificationExclusion},
|
||||
{"SecurityMeasures", data.SecurityMeasures},
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
fieldConfig := getSOAFieldConfig(field.name)
|
||||
if fieldConfig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
filterColumns, err := processField(f, sheetName, row, field.value, *fieldConfig, isFirstRow)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot write field %s: %w", field.name, err)
|
||||
}
|
||||
|
||||
allFilterColumns = append(allFilterColumns, filterColumns...)
|
||||
}
|
||||
|
||||
return allFilterColumns, nil
|
||||
}
|
||||
|
||||
func getSOAFieldConfig(fieldName string) *FieldConfiguration {
|
||||
for _, config := range soaFieldConfigs {
|
||||
if config.Field == fieldName {
|
||||
return &config
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applySOAAutoFilter(f *excelize.File, sheetName string, filterColumns []string) error {
|
||||
if len(filterColumns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
firstCol, lastCol := filterColumns[0], filterColumns[0]
|
||||
for _, col := range filterColumns {
|
||||
if col < firstCol {
|
||||
firstCol = col
|
||||
}
|
||||
if col > lastCol {
|
||||
lastCol = col
|
||||
}
|
||||
}
|
||||
|
||||
filterRange := fmt.Sprintf("%s7:%s1000", firstCol, lastCol)
|
||||
return f.AutoFilter(sheetName, filterRange, []excelize.AutoFilterOptions{})
|
||||
}
|
||||
|
||||
func applySOAFinalFormatting(f *excelize.File, sheetName string, dataRowCount int) error {
|
||||
if dataRowCount > 0 {
|
||||
headerRowHeight := 30.0
|
||||
f.SetRowHeight(sheetName, 6, headerRowHeight)
|
||||
f.SetRowHeight(sheetName, 7, headerRowHeight)
|
||||
|
||||
return f.SetPanes(sheetName, &excelize.Panes{
|
||||
Freeze: true,
|
||||
XSplit: 0,
|
||||
YSplit: 7,
|
||||
TopLeftCell: "A8",
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processField(f *excelize.File, sheetName string, row int, value interface{}, config FieldConfiguration, isFirstRow bool) ([]string, error) {
|
||||
switch v := value.(type) {
|
||||
case ExcelMarshaler:
|
||||
marshaler := v
|
||||
excelValue := marshaler.MarshalExcel()
|
||||
|
||||
styleID, err := createCellStyle(f, excelValue.Style)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writeSingleColumnField(f, sheetName, row, excelValue, &config, styleID, isFirstRow)
|
||||
|
||||
case string:
|
||||
if len(config.Columns) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
col := config.Columns[0]
|
||||
cellRef := fmt.Sprintf("%s%d", col, row)
|
||||
|
||||
f.SetCellValue(sheetName, cellRef, v)
|
||||
|
||||
textStyleID, err := createCellStyle(f, getTextStyle())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create text style: %w", err)
|
||||
}
|
||||
f.SetCellStyle(sheetName, cellRef, cellRef, textStyleID)
|
||||
|
||||
if isFirstRow {
|
||||
width := config.DefaultWidth
|
||||
if len(config.Width) > 0 {
|
||||
width = config.Width[0]
|
||||
}
|
||||
f.SetColWidth(sheetName, col, col, width)
|
||||
}
|
||||
|
||||
if config.HasFilter {
|
||||
if len(config.FilterColumns) > 0 {
|
||||
return config.FilterColumns, nil
|
||||
}
|
||||
return []string{col}, nil
|
||||
}
|
||||
return []string{}, nil
|
||||
|
||||
case []string:
|
||||
var formattedLines []string
|
||||
for _, item := range v {
|
||||
lines := strings.Split(item, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
formattedLines = append(formattedLines, "• "+strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
joinedValue := strings.Join(formattedLines, "\n")
|
||||
|
||||
if len(config.Columns) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
col := config.Columns[0]
|
||||
cellRef := fmt.Sprintf("%s%d", col, row)
|
||||
|
||||
f.SetCellValue(sheetName, cellRef, joinedValue)
|
||||
|
||||
textStyleID, err := createCellStyle(f, getTextStyle())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create text style: %w", err)
|
||||
}
|
||||
f.SetCellStyle(sheetName, cellRef, cellRef, textStyleID)
|
||||
|
||||
if isFirstRow {
|
||||
width := config.DefaultWidth
|
||||
if len(config.Width) > 0 {
|
||||
width = config.Width[0]
|
||||
}
|
||||
f.SetColWidth(sheetName, col, col, width)
|
||||
}
|
||||
|
||||
if config.HasFilter {
|
||||
if len(config.FilterColumns) > 0 {
|
||||
return config.FilterColumns, nil
|
||||
}
|
||||
return []string{col}, nil
|
||||
}
|
||||
return []string{}, nil
|
||||
|
||||
case *bool:
|
||||
yesNoValue := boolToYesNo(v)
|
||||
excelValue := yesNoValue.MarshalExcel()
|
||||
|
||||
styleID, err := createCellStyle(f, excelValue.Style)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writeSingleColumnField(f, sheetName, row, excelValue, &config, styleID, isFirstRow)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("no handler found for field type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateExcel(data SOAData) ([]byte, error) {
|
||||
return GenerateSOAExcel(data)
|
||||
}
|
||||
|
||||
func writeSingleColumnField(f *excelize.File, sheetName string, row int, excelValue ExcelValue, colDef *FieldConfiguration, styleID int, isFirstRow bool) ([]string, error) {
|
||||
if len(colDef.Columns) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
col := colDef.Columns[0]
|
||||
cellRef := fmt.Sprintf("%s%d", col, row)
|
||||
|
||||
f.SetCellValue(sheetName, cellRef, excelValue.Value)
|
||||
f.SetCellStyle(sheetName, cellRef, cellRef, styleID)
|
||||
|
||||
if isFirstRow {
|
||||
if err := applyDataValidation(f, sheetName, col, row, excelValue.Validation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := setColumnWidth(f, sheetName, col, excelValue.Width); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if colDef.HasFilter {
|
||||
if len(colDef.FilterColumns) > 0 {
|
||||
return colDef.FilterColumns, nil
|
||||
}
|
||||
return []string{col}, nil
|
||||
}
|
||||
return []string{}, nil
|
||||
}
|
||||
129
pkg/soagen/header.go
Normal file
129
pkg/soagen/header.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type HeaderCell struct {
|
||||
Cell string
|
||||
Value string
|
||||
Style string // "header" or "cell"
|
||||
}
|
||||
|
||||
type MergedCell struct {
|
||||
StartCell string
|
||||
EndCell string
|
||||
}
|
||||
|
||||
type HeaderLayout struct {
|
||||
InfoHeader []HeaderCell
|
||||
InfoData []HeaderCell
|
||||
MainHeader []HeaderCell
|
||||
SubHeader []HeaderCell
|
||||
Merges []MergedCell
|
||||
}
|
||||
|
||||
func getSOAHeaderLayout() HeaderLayout {
|
||||
return HeaderLayout{
|
||||
InfoHeader: []HeaderCell{
|
||||
{"A2", "Version", "header"},
|
||||
{"C2", "Date", "header"},
|
||||
{"D2", "Comment", "header"},
|
||||
{"E2", "Author", "header"},
|
||||
{"F2", "Approver", "header"},
|
||||
},
|
||||
InfoData: []HeaderCell{
|
||||
{"A3", "1.0", "cell"},
|
||||
{"C3", time.Now().Format("01/02/2006"), "cell"},
|
||||
{"D3", "Initial SoA", "cell"},
|
||||
{"E3", "System Admin", "cell"},
|
||||
{"F3", "Security Manager", "cell"},
|
||||
},
|
||||
MainHeader: []HeaderCell{
|
||||
{"A6", "Control", "header"},
|
||||
{"B6", "Control name", "header"},
|
||||
{"C6", "Applicability", "header"},
|
||||
{"D6", "Justification for exclusion", "header"},
|
||||
{"E6", "Justification for inclusion", "header"},
|
||||
{"F6", "Justification for inclusion", "header"},
|
||||
{"G6", "Justification for inclusion", "header"},
|
||||
{"H6", "Justification for inclusion", "header"},
|
||||
{"I6", "List of security measure or policy", "header"},
|
||||
},
|
||||
SubHeader: []HeaderCell{
|
||||
{"A7", "Control", "header"},
|
||||
{"B7", "Control name", "header"},
|
||||
{"C7", "Applicability", "header"},
|
||||
{"D7", "Justification for exclusion", "header"},
|
||||
{"E7", "Regulatory", "header"},
|
||||
{"F7", "Contractual", "header"},
|
||||
{"G7", "Best practice", "header"},
|
||||
{"H7", "Risk assessment", "header"},
|
||||
{"I7", "List of security measure or policy", "header"},
|
||||
},
|
||||
Merges: []MergedCell{
|
||||
{"A6", "A7"},
|
||||
{"B6", "B7"},
|
||||
{"C6", "C7"},
|
||||
{"D6", "D7"},
|
||||
{"E6", "H6"},
|
||||
{"I6", "I7"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func applyHeaderLayout(f *excelize.File, sheetName string, layout HeaderLayout, headerStyle, cellStyle int) error {
|
||||
if err := applyCells(f, sheetName, layout.InfoHeader, headerStyle, cellStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyCells(f, sheetName, layout.InfoData, headerStyle, cellStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyCells(f, sheetName, layout.MainHeader, headerStyle, cellStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyCells(f, sheetName, layout.SubHeader, headerStyle, cellStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, merge := range layout.Merges {
|
||||
if err := f.MergeCell(sheetName, merge.StartCell, merge.EndCell); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyCells(f *excelize.File, sheetName string, cells []HeaderCell, headerStyle, cellStyle int) error {
|
||||
for _, cell := range cells {
|
||||
f.SetCellValue(sheetName, cell.Cell, cell.Value)
|
||||
|
||||
style := cellStyle
|
||||
if cell.Style == "header" {
|
||||
style = headerStyle
|
||||
}
|
||||
|
||||
f.SetCellStyle(sheetName, cell.Cell, cell.Cell, style)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
33
pkg/soagen/soa_data.go
Normal file
33
pkg/soagen/soa_data.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
// SOARowData represents a single row in the State of Applicability Excel
|
||||
type SOARowData struct {
|
||||
SectionTitle string
|
||||
ControlName string
|
||||
Applicability Applicability
|
||||
Regulatory *bool
|
||||
Contractual *bool
|
||||
BestPractice *bool
|
||||
RiskAssessment *bool
|
||||
JustificationExclusion string
|
||||
SecurityMeasures []string
|
||||
}
|
||||
|
||||
// SOAData contains all the data needed for State of Applicability generation
|
||||
type SOAData struct {
|
||||
Rows []SOARowData
|
||||
}
|
||||
89
pkg/soagen/styles.go
Normal file
89
pkg/soagen/styles.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// getTextStyle returns the standard text style for string fields
|
||||
func getTextStyle() *excelize.Style {
|
||||
return &excelize.Style{
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "#000000", Style: 1},
|
||||
{Type: "top", Color: "#000000", Style: 1},
|
||||
{Type: "bottom", Color: "#000000", Style: 1},
|
||||
{Type: "right", Color: "#000000", Style: 1},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center", WrapText: true},
|
||||
}
|
||||
}
|
||||
|
||||
// getHeaderStyle returns the standard header style
|
||||
func getHeaderStyle() *excelize.Style {
|
||||
return &excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Size: 10, Color: "#000000"},
|
||||
Fill: excelize.Fill{Type: "pattern", Color: []string{"#D9D9D9"}, Pattern: 1},
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "#000000", Style: 1},
|
||||
{Type: "top", Color: "#000000", Style: 1},
|
||||
{Type: "bottom", Color: "#000000", Style: 1},
|
||||
{Type: "right", Color: "#000000", Style: 1},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
|
||||
}
|
||||
}
|
||||
|
||||
// getCellStyle returns the standard cell style
|
||||
func getCellStyle() *excelize.Style {
|
||||
return &excelize.Style{
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "#000000", Style: 1},
|
||||
{Type: "top", Color: "#000000", Style: 1},
|
||||
{Type: "bottom", Color: "#000000", Style: 1},
|
||||
{Type: "right", Color: "#000000", Style: 1},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
|
||||
}
|
||||
}
|
||||
|
||||
func createCellStyle(f *excelize.File, style *excelize.Style) (int, error) {
|
||||
styleID, err := f.NewStyle(style)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot create style: %w", err)
|
||||
}
|
||||
return styleID, nil
|
||||
}
|
||||
|
||||
func applyDataValidation(f *excelize.File, sheetName, col string, row int, validation []string) error {
|
||||
if len(validation) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dv := excelize.NewDataValidation(true)
|
||||
dv.Sqref = fmt.Sprintf("%s%d:%s1000", col, row, col) // Apply to reasonable range
|
||||
dv.SetDropList(validation)
|
||||
dv.SetError(excelize.DataValidationErrorStyleStop, "Invalid Input", "Please select from the dropdown list.")
|
||||
return f.AddDataValidation(sheetName, dv)
|
||||
}
|
||||
|
||||
func setColumnWidth(f *excelize.File, sheetName, col string, width float64) error {
|
||||
if width > 0 {
|
||||
f.SetColWidth(sheetName, col, col, width)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
60
pkg/soagen/yesno.go
Normal file
60
pkg/soagen/yesno.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package soagen
|
||||
|
||||
import "github.com/xuri/excelize/v2"
|
||||
|
||||
// YesNo represents a YES/NO/Empty value
|
||||
type YesNo string
|
||||
|
||||
const (
|
||||
Yes YesNo = "YES"
|
||||
No YesNo = "NO"
|
||||
Empty YesNo = ""
|
||||
)
|
||||
|
||||
// String returns the string representation of YesNo
|
||||
func (yn YesNo) String() string {
|
||||
return string(yn)
|
||||
}
|
||||
|
||||
// MarshalExcel implements ExcelMarshaler for YesNo
|
||||
func (yn YesNo) MarshalExcel() ExcelValue {
|
||||
return ExcelValue{
|
||||
Value: yn.String(),
|
||||
Style: &excelize.Style{
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "#000000", Style: 1},
|
||||
{Type: "top", Color: "#000000", Style: 1},
|
||||
{Type: "bottom", Color: "#000000", Style: 1},
|
||||
{Type: "right", Color: "#000000", Style: 1},
|
||||
},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
|
||||
},
|
||||
Validation: []string{string(Yes), string(No), string(Empty)},
|
||||
Width: 12,
|
||||
}
|
||||
}
|
||||
|
||||
// boolToYesNo converts *bool to YesNo type for Excel output
|
||||
func boolToYesNo(b *bool) YesNo {
|
||||
if b == nil {
|
||||
return Empty
|
||||
}
|
||||
if *b {
|
||||
return Yes
|
||||
}
|
||||
return No
|
||||
}
|
||||
Reference in New Issue
Block a user