The source headers, LICENSE files, and license metadata had drifted apart. Align the entire project to MIT: - Convert every source-file header to the MIT text across all comment styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including SPDX-License-Identifier tags - Set the root and cookie-banner LICENSE files to the MIT text with a "MIT License" title line - Switch the package.json license fields, Docker image label, and cookie-banner README to MIT - Update docs and the genmodels header generator accordingly - Normalize copyright lines to a single format (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the hello@getprobo.com and hello@probo.inc emails to hello@probo.com and the comma-separated years to a hyphenated range Genuine third-party references are intentionally left untouched: the Lucide icon attributions (Lucide is ISC) and the trivy dependency license allowlist. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
440 lines
12 KiB
Go
440 lines
12 KiB
Go
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package probo
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.gearno.de/kit/pg"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/gid"
|
|
"go.probo.inc/probo/pkg/page"
|
|
"go.probo.inc/probo/pkg/validator"
|
|
)
|
|
|
|
type StatementOfApplicabilityService struct {
|
|
svc *Service
|
|
}
|
|
|
|
type (
|
|
CreateStatementOfApplicabilityRequest struct {
|
|
OrganizationID gid.GID
|
|
Name string
|
|
}
|
|
|
|
UpdateStatementOfApplicabilityRequest struct {
|
|
StatementOfApplicabilityID gid.GID
|
|
Name *string
|
|
}
|
|
)
|
|
|
|
func (csr *CreateStatementOfApplicabilityRequest) Validate() error {
|
|
v := validator.New()
|
|
|
|
v.Check(csr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
|
|
v.Check(csr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
|
|
|
return v.Error()
|
|
}
|
|
|
|
func (usr *UpdateStatementOfApplicabilityRequest) Validate() error {
|
|
v := validator.New()
|
|
|
|
v.Check(usr.StatementOfApplicabilityID, "statement_of_applicability_id", validator.Required(), validator.GID(coredata.StatementOfApplicabilityEntityType))
|
|
v.Check(usr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
|
|
|
return v.Error()
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) ListForOrganizationID(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
organizationID gid.GID,
|
|
cursor *page.Cursor[coredata.StatementOfApplicabilityOrderField],
|
|
) (*page.Page[*coredata.StatementOfApplicability, coredata.StatementOfApplicabilityOrderField], error) {
|
|
var statementsOfApplicability coredata.StatementsOfApplicability
|
|
|
|
organization := &coredata.Organization{}
|
|
|
|
err := s.svc.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) error {
|
|
if err := organization.LoadByID(ctx, conn, scope, organizationID); err != nil {
|
|
return fmt.Errorf("cannot load organization: %w", err)
|
|
}
|
|
|
|
err := statementsOfApplicability.LoadByOrganizationID(
|
|
ctx,
|
|
conn,
|
|
scope,
|
|
organization.ID,
|
|
cursor,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot load statements_of_applicability: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return page.NewPage(statementsOfApplicability, cursor), nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) CountForOrganizationID(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
organizationID gid.GID,
|
|
) (int, error) {
|
|
var count int
|
|
|
|
err := s.svc.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) (err error) {
|
|
statementsOfApplicability := &coredata.StatementsOfApplicability{}
|
|
|
|
count, err = statementsOfApplicability.CountByOrganizationID(ctx, conn, scope, organizationID)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot count statements_of_applicability: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) Get(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
statementOfApplicabilityID gid.GID,
|
|
) (*coredata.StatementOfApplicability, error) {
|
|
statementOfApplicability := &coredata.StatementOfApplicability{}
|
|
|
|
err := s.svc.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) error {
|
|
return statementOfApplicability.LoadByID(ctx, conn, scope, statementOfApplicabilityID)
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return statementOfApplicability, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) Create(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
req CreateStatementOfApplicabilityRequest,
|
|
) (*coredata.StatementOfApplicability, error) {
|
|
if err := req.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid request: %w", err)
|
|
}
|
|
|
|
now := time.Now()
|
|
organization := &coredata.Organization{}
|
|
|
|
err := s.svc.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) error {
|
|
return organization.LoadByID(ctx, conn, scope, req.OrganizationID)
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot load organization: %w", err)
|
|
}
|
|
|
|
statementOfApplicabilityID := gid.New(organization.ID.TenantID(), coredata.StatementOfApplicabilityEntityType)
|
|
statementOfApplicability := &coredata.StatementOfApplicability{
|
|
ID: statementOfApplicabilityID,
|
|
OrganizationID: organization.ID,
|
|
Name: req.Name,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
err = s.svc.pg.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Tx) error {
|
|
if err := statementOfApplicability.Insert(ctx, conn, scope); err != nil {
|
|
return fmt.Errorf("cannot insert statement_of_applicability: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return statementOfApplicability, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) Update(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
req UpdateStatementOfApplicabilityRequest,
|
|
) (*coredata.StatementOfApplicability, error) {
|
|
if err := req.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid request: %w", err)
|
|
}
|
|
|
|
statementOfApplicability := &coredata.StatementOfApplicability{}
|
|
|
|
err := s.svc.pg.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Tx) error {
|
|
if err := statementOfApplicability.LoadByID(ctx, conn, scope, req.StatementOfApplicabilityID); err != nil {
|
|
return fmt.Errorf("cannot load statement_of_applicability: %w", err)
|
|
}
|
|
|
|
if req.Name != nil {
|
|
statementOfApplicability.Name = *req.Name
|
|
}
|
|
|
|
statementOfApplicability.UpdatedAt = time.Now()
|
|
|
|
if err := statementOfApplicability.Update(ctx, conn, scope); err != nil {
|
|
return fmt.Errorf("cannot update statement_of_applicability: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return statementOfApplicability, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) Delete(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
statementOfApplicabilityID gid.GID,
|
|
) error {
|
|
statementOfApplicability := &coredata.StatementOfApplicability{ID: statementOfApplicabilityID}
|
|
|
|
err := s.svc.pg.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Tx) error {
|
|
if err := statementOfApplicability.LoadByID(ctx, conn, scope, statementOfApplicabilityID); err != nil {
|
|
return fmt.Errorf("cannot load statement_of_applicability: %w", err)
|
|
}
|
|
|
|
if err := statementOfApplicability.Delete(ctx, conn, scope); err != nil {
|
|
return fmt.Errorf("cannot delete statement_of_applicability: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) GetApplicabilityStatement(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
applicabilityStatementID gid.GID,
|
|
) (*coredata.ApplicabilityStatement, error) {
|
|
applicabilityStatement := &coredata.ApplicabilityStatement{}
|
|
|
|
err := s.svc.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) error {
|
|
return applicabilityStatement.LoadByID(ctx, conn, scope, applicabilityStatementID)
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return applicabilityStatement, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) ListApplicabilityStatements(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
statementOfApplicabilityID gid.GID,
|
|
cursor *page.Cursor[coredata.ApplicabilityStatementOrderField],
|
|
) (*page.Page[*coredata.ApplicabilityStatement, coredata.ApplicabilityStatementOrderField], error) {
|
|
var statements coredata.ApplicabilityStatements
|
|
|
|
err := s.svc.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) error {
|
|
if err := statements.LoadByStatementOfApplicabilityID(ctx, conn, scope, statementOfApplicabilityID, cursor); err != nil {
|
|
return fmt.Errorf("cannot load applicability statements: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return page.NewPage(statements, cursor), nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) CountApplicabilityStatements(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
statementOfApplicabilityID gid.GID,
|
|
) (int, error) {
|
|
var count int
|
|
|
|
err := s.svc.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) (err error) {
|
|
statements := &coredata.ApplicabilityStatements{}
|
|
|
|
count, err = statements.CountByStatementOfApplicabilityID(ctx, conn, scope, statementOfApplicabilityID)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot count applicability statements: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) CreateApplicabilityStatement(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
statementOfApplicabilityID gid.GID,
|
|
controlID gid.GID,
|
|
applicability bool,
|
|
justification *string,
|
|
) (*coredata.ApplicabilityStatement, error) {
|
|
var (
|
|
statementOfApplicability = &coredata.StatementOfApplicability{}
|
|
applicabilityStatement = &coredata.ApplicabilityStatement{}
|
|
now = time.Now()
|
|
)
|
|
|
|
err := s.svc.pg.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Tx) error {
|
|
if err := statementOfApplicability.LoadByID(ctx, conn, scope, statementOfApplicabilityID); err != nil {
|
|
return fmt.Errorf("cannot load statement of applicability: %w", err)
|
|
}
|
|
|
|
control := &coredata.Control{}
|
|
if err := control.LoadByID(ctx, conn, scope, controlID); err != nil {
|
|
return fmt.Errorf("cannot load control: %w", err)
|
|
}
|
|
|
|
applicabilityStatement = &coredata.ApplicabilityStatement{
|
|
ID: gid.New(scope.GetTenantID(), coredata.ApplicabilityStatementEntityType),
|
|
StatementOfApplicabilityID: statementOfApplicabilityID,
|
|
ControlID: controlID,
|
|
OrganizationID: statementOfApplicability.OrganizationID,
|
|
Applicability: applicability,
|
|
Justification: justification,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if err := applicabilityStatement.Insert(ctx, conn, scope); err != nil {
|
|
return fmt.Errorf("cannot insert applicability statement: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return applicabilityStatement, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) UpdateApplicabilityStatement(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
applicabilityStatementID gid.GID,
|
|
applicability bool,
|
|
justification *string,
|
|
) (*coredata.ApplicabilityStatement, error) {
|
|
applicabilityStatement := &coredata.ApplicabilityStatement{}
|
|
|
|
err := s.svc.pg.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Tx) error {
|
|
if err := applicabilityStatement.LoadByID(ctx, conn, scope, applicabilityStatementID); err != nil {
|
|
return err
|
|
}
|
|
|
|
applicabilityStatement.Applicability = applicability
|
|
applicabilityStatement.Justification = justification
|
|
applicabilityStatement.UpdatedAt = time.Now()
|
|
|
|
return applicabilityStatement.UpdateByID(ctx, conn, scope)
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return applicabilityStatement, nil
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) DeleteApplicabilityStatement(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
applicabilityStatementID gid.GID,
|
|
) error {
|
|
applicabilityStatement := &coredata.ApplicabilityStatement{}
|
|
|
|
return s.svc.pg.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Tx) error {
|
|
return applicabilityStatement.DeleteByID(ctx, conn, scope, applicabilityStatementID)
|
|
},
|
|
)
|
|
}
|
|
|
|
func (s StatementOfApplicabilityService) ListControlLinks(
|
|
ctx context.Context, scope coredata.Scoper,
|
|
controlID gid.GID,
|
|
cursor *page.Cursor[coredata.ApplicabilityStatementOrderField],
|
|
) (*page.Page[*coredata.ApplicabilityStatement, coredata.ApplicabilityStatementOrderField], error) {
|
|
var controls coredata.ApplicabilityStatements
|
|
|
|
err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return controls.LoadByControlID(ctx, conn, scope, controlID, cursor)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return page.NewPage(controls, cursor), nil
|
|
}
|