diff --git a/internal/v1/api.go b/internal/v1/api.go index fe331e722..7bd869fd5 100644 --- a/internal/v1/api.go +++ b/internal/v1/api.go @@ -289,10 +289,11 @@ type BlueprintItem struct { Version int `json:"version"` } -// BlueprintLint Linting errors in the current blueprint, these might need to be resolved before the -// blueprint can be used to build images again. +// BlueprintLint Linting errors and warnings in the current blueprint. Errors might need to be resolved before the +// blueprint can be used to build images again. Warnings provide information about policy changes. type BlueprintLint struct { - Errors []BlueprintLintItem `json:"errors"` + Errors []BlueprintLintItem `json:"errors"` + Warnings []BlueprintLintItem `json:"warnings"` } // BlueprintLintItem defines model for BlueprintLintItem. @@ -323,8 +324,8 @@ type BlueprintResponse struct { // ImageRequests Array of image requests. Having more image requests in a single blueprint is currently not supported. ImageRequests []ImageRequest `json:"image_requests"` - // Lint Linting errors in the current blueprint, these might need to be resolved before the - // blueprint can be used to build images again. + // Lint Linting errors and warnings in the current blueprint. Errors might need to be resolved before the + // blueprint can be used to build images again. Warnings provide information about policy changes. Lint BlueprintLint `json:"lint"` Name string `json:"name"` } @@ -2023,6 +2024,9 @@ type ServerInterface interface { // Apply linter fixes to blueprint // (POST /experimental/blueprints/{id}/fixup) FixupBlueprint(ctx echo.Context, id openapi_types.UUID) error + // Ignore blueprint compliance warnings + // (POST /experimental/blueprints/{id}/ignore-warnings) + IgnoreBlueprintWarnings(ctx echo.Context, id openapi_types.UUID) error // List recommended packages. // (POST /experimental/recommendations) RecommendPackage(ctx echo.Context) error @@ -2428,6 +2432,22 @@ func (w *ServerInterfaceWrapper) FixupBlueprint(ctx echo.Context) error { return err } +// IgnoreBlueprintWarnings converts echo context to params. +func (w *ServerInterfaceWrapper) IgnoreBlueprintWarnings(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.IgnoreBlueprintWarnings(ctx, id) + return err +} + // RecommendPackage converts echo context to params. func (w *ServerInterfaceWrapper) RecommendPackage(ctx echo.Context) error { var err error @@ -2612,6 +2632,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/composes/:composeId/metadata", wrapper.GetComposeMetadata) router.GET(baseURL+"/distributions", wrapper.GetDistributions) router.POST(baseURL+"/experimental/blueprints/:id/fixup", wrapper.FixupBlueprint) + router.POST(baseURL+"/experimental/blueprints/:id/ignore-warnings", wrapper.IgnoreBlueprintWarnings) router.POST(baseURL+"/experimental/recommendations", wrapper.RecommendPackage) router.GET(baseURL+"/oscap/:distribution/profiles", wrapper.GetOscapProfiles) router.GET(baseURL+"/oscap/:distribution/:profile/customizations", wrapper.GetOscapCustomizations) diff --git a/internal/v1/api.yaml b/internal/v1/api.yaml index 46e23e8c4..0c86d1f38 100644 --- a/internal/v1/api.yaml +++ b/internal/v1/api.yaml @@ -769,6 +769,27 @@ paths: description: successful update 404: description: blueprint was not found + /experimental/blueprints/{id}/ignore-warnings: + parameters: + - in: path + name: id + schema: + type: string + format: uuid + example: '123e4567-e89b-12d3-a456-426655440000' + required: true + description: UUID of a blueprint + post: + summary: Ignore blueprint compliance warnings + operationId: ignoreBlueprintWarnings + description: | + Ignore compliance warnings by saving the current blueprint state as the new baseline. + This will prevent the same warnings from appearing in future linting checks. + responses: + 201: + description: warnings successfully ignored + 404: + description: blueprint was not found components: schemas: @@ -1151,14 +1172,19 @@ components: BlueprintLint: required: - errors + - warnings description: | - Linting errors in the current blueprint, these might need to be resolved before the - blueprint can be used to build images again. + Linting errors and warnings in the current blueprint. Errors might need to be resolved before the + blueprint can be used to build images again. Warnings provide information about policy changes. properties: errors: type: array items: $ref: '#/components/schemas/BlueprintLintItem' + warnings: + type: array + items: + $ref: '#/components/schemas/BlueprintLintItem' BlueprintLintItem: type: object required: diff --git a/internal/v1/handler_blueprints.go b/internal/v1/handler_blueprints.go index 152e51486..2b49c13d5 100644 --- a/internal/v1/handler_blueprints.go +++ b/internal/v1/handler_blueprints.go @@ -162,7 +162,7 @@ func (h *Handlers) buildServiceSnapshots(ctx echo.Context, customizations *Custo } var cust Customizations - _, err = h.lintOpenscap(ctx, &cust, true, distribution, compl.PolicyId.String()) + _, _, err = h.lintOpenscap(ctx, &cust, true, distribution, compl.PolicyId.String(), nil) if err != nil { slog.ErrorContext(ctx.Request().Context(), "error getting policy customizations via lintOpenscap", "error", err.Error(), "distribution", distribution, "policy_id", compl.PolicyId.String()) @@ -385,7 +385,7 @@ func (h *Handlers) GetBlueprint(ctx echo.Context, id openapi_types.UUID, params return err } - lintErrors, err := h.lintBlueprint(ctx, &blueprint, false) + lintErrors, lintWarnings, err := h.lintBlueprint(ctx, blueprintEntry, false) if err != nil { return err } @@ -398,33 +398,71 @@ func (h *Handlers) GetBlueprint(ctx echo.Context, id openapi_types.UUID, params Distribution: blueprint.Distribution, Customizations: blueprint.Customizations, Lint: BlueprintLint{ - Errors: lintErrors, + Errors: lintErrors, + Warnings: lintWarnings, }, } return ctx.JSON(http.StatusOK, blueprintResponse) } -func (h *Handlers) lintBlueprint(ctx echo.Context, blueprint *BlueprintBody, fixup bool) ([]BlueprintLintItem, error) { +func (h *Handlers) lintBlueprint(ctx echo.Context, bpe *db.BlueprintEntry, fixup bool) ([]BlueprintLintItem, []BlueprintLintItem, error) { lintErrors := []BlueprintLintItem{} - if blueprint.Customizations.Openscap != nil { - var compl OpenSCAPCompliance - var err error - if compl, err = blueprint.Customizations.Openscap.AsOpenSCAPCompliance(); err == nil && compl.PolicyId != uuid.Nil { - errs, err := h.lintOpenscap(ctx, &blueprint.Customizations, fixup, blueprint.Distribution, compl.PolicyId.String()) - if err == compliance.ErrorTailoringNotFound { - lintErrors = append(lintErrors, BlueprintLintItem{ - Name: "Compliance", - Description: "Compliance policy does not have a definition for the latest minor version", - }) - } else if err != nil { - return nil, err - } else { - lintErrors = append(lintErrors, errs...) - } - } + lintWarnings := []BlueprintLintItem{} + + bpBody, err := BlueprintFromEntry(bpe) + if err != nil { + return nil, nil, err + } + + if bpBody.Customizations.Openscap == nil { + return lintErrors, lintWarnings, nil + } + + compl, err := bpBody.Customizations.Openscap.AsOpenSCAPCompliance() + if err != nil || compl.PolicyId == uuid.Nil { + return lintErrors, lintWarnings, nil + } + + snapshotCustomizations := h.extractSnapshotCustomizations(bpe) + + errs, warns, err := h.lintOpenscap(ctx, &bpBody.Customizations, fixup, bpBody.Distribution, compl.PolicyId.String(), snapshotCustomizations) + if err == compliance.ErrorTailoringNotFound { + lintErrors = append(lintErrors, BlueprintLintItem{ + Name: "Compliance", + Description: "Compliance policy does not have a definition for the latest minor version", + }) + } else if err != nil { + return nil, nil, err + } else { + lintErrors = append(lintErrors, errs...) + lintWarnings = append(lintWarnings, warns...) } - return lintErrors, nil + + return lintErrors, lintWarnings, nil +} + +// Helper function to extract saved policy from service snapshots +func (h *Handlers) extractSnapshotCustomizations(bpe *db.BlueprintEntry) *Customizations { + if len(bpe.ServiceSnapshots) == 0 { + return nil + } + + var snapshots db.ServiceSnapshots + if err := json.Unmarshal(bpe.ServiceSnapshots, &snapshots); err != nil { + return nil + } + + if snapshots.Compliance == nil || len(snapshots.Compliance.PolicyCustomizations) == 0 { + return nil + } + + var savedCust Customizations + if err := json.Unmarshal(snapshots.Compliance.PolicyCustomizations, &savedCust); err != nil { + return nil + } + + return &savedCust } func (h *Handlers) ExportBlueprint(ctx echo.Context, id openapi_types.UUID) error { @@ -841,9 +879,15 @@ func (h *Handlers) FixupBlueprint(ctx echo.Context, id openapi_types.UUID) error return err } - _, err = h.lintBlueprint(ctx, &blueprint, true) - if err != nil { - return err + if blueprint.Customizations.Openscap != nil { + compl, err := blueprint.Customizations.Openscap.AsOpenSCAPCompliance() + if err == nil && compl.PolicyId != uuid.Nil { + snapshotCustomizations := h.extractSnapshotCustomizations(blueprintEntry) + _, _, err = h.lintOpenscap(ctx, &blueprint.Customizations, true, blueprint.Distribution, compl.PolicyId.String(), snapshotCustomizations) + if err != nil && err != compliance.ErrorTailoringNotFound { + return err + } + } } var md BlueprintMetadata @@ -902,3 +946,118 @@ func (h *Handlers) FixupBlueprint(ctx echo.Context, id openapi_types.UUID) error return ctx.NoContent(http.StatusCreated) } + +func (h *Handlers) IgnoreBlueprintWarnings(ctx echo.Context, id openapi_types.UUID) error { + userID, err := h.server.getIdentity(ctx) + if err != nil { + return err + } + + blueprintEntry, err := h.server.db.GetBlueprint(ctx.Request().Context(), id, userID.OrgID(), nil) + if err != nil { + if errors.Is(err, db.ErrBlueprintNotFound) { + return echo.NewHTTPError(http.StatusNotFound, err) + } + return err + } + + blueprint, err := BlueprintFromEntry( + blueprintEntry, + WithRedactedPasswords(), + ) + if err != nil { + return err + } + + // Only proceed if this blueprint has compliance configured + if blueprint.Customizations.Openscap == nil { + slog.InfoContext(ctx.Request().Context(), "ignoring warnings skipped - no compliance configured", + "blueprint_id", blueprintEntry.Id) + return ctx.NoContent(http.StatusCreated) + } + + compl, err := blueprint.Customizations.Openscap.AsOpenSCAPCompliance() + if err != nil || compl.PolicyId == uuid.Nil { + slog.InfoContext(ctx.Request().Context(), "ignoring warnings skipped - invalid compliance config", + "blueprint_id", blueprintEntry.Id, + "error", err) + return ctx.NoContent(http.StatusCreated) + } + + // Get current policy customizations to save as the baseline for future comparisons + d, err := h.server.getDistro(ctx, blueprint.Distribution) + if err != nil { + return err + } + major, minor, err := d.RHELMajorMinor() + if err != nil { + return err + } + + // Get the current policy requirements (for validation only) + _, err = h.server.complianceClient.PolicyCustomizations(ctx.Request().Context(), major, minor, compl.PolicyId.String()) + if err != nil { + return err + } + + // Convert current policy customizations to JSON for storage + var policyCustomizations Customizations + _, _, err = h.lintOpenscap(ctx, &policyCustomizations, true, blueprint.Distribution, compl.PolicyId.String(), nil) + if err != nil { + return err + } + + policyCustomizationsJSON, err := json.Marshal(policyCustomizations) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal policy customizations") + } + + // Create service snapshots with current policy requirements + serviceSnapshots := &db.ServiceSnapshots{ + Compliance: &db.ComplianceSnapshot{ + PolicyId: compl.PolicyId, + PolicyCustomizations: json.RawMessage(policyCustomizationsJSON), + }, + } + + serviceSnapshotsJSON, err := json.Marshal(serviceSnapshots) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to marshal service snapshots") + } + + var md BlueprintMetadata + if len(blueprintEntry.Metadata) > 0 { + err = json.Unmarshal(blueprintEntry.Metadata, &md) + if err != nil { + return err + } + } + + blueprintRequest := CreateBlueprintRequest{ + Name: blueprintEntry.Name, + Description: &blueprintEntry.Description, + Metadata: &md, + Distribution: blueprint.Distribution, + ImageRequests: blueprint.ImageRequests, + Customizations: blueprint.Customizations, + } + + body, err := json.Marshal(blueprintRequest) + if err != nil { + return err + } + desc := common.FromPtr(blueprintRequest.Description) + + versionId := uuid.New() + err = h.server.db.UpdateBlueprint(ctx.Request().Context(), versionId, blueprintEntry.Id, userID.OrgID(), blueprintRequest.Name, desc, body, serviceSnapshotsJSON) + if err != nil { + slog.ErrorContext(ctx.Request().Context(), "error updating blueprint in db during ignore warnings", + "error", err) + if errors.Is(err, db.ErrBlueprintNotFound) { + return echo.NewHTTPError(http.StatusNotFound, err) + } + return err + } + + return ctx.NoContent(http.StatusCreated) +} diff --git a/internal/v1/handler_oscap.go b/internal/v1/handler_oscap.go index a1b07746b..c5b9657fc 100644 --- a/internal/v1/handler_oscap.go +++ b/internal/v1/handler_oscap.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/labstack/echo/v4" + "github.com/osbuild/blueprint/pkg/blueprint" "github.com/osbuild/image-builder-crc/internal/clients/compliance" "github.com/osbuild/image-builder-crc/internal/common" @@ -181,7 +182,7 @@ func (h *Handlers) GetOscapCustomizations(ctx echo.Context, distribution Distrib func (h *Handlers) GetOscapCustomizationsForPolicy(ctx echo.Context, policy uuid.UUID, distro Distributions) error { var cust Customizations - _, err := h.lintOpenscap(ctx, &cust, true, distro, policy.String()) + _, _, err := h.lintOpenscap(ctx, &cust, true, distro, policy.String(), nil) if err == distribution.ErrMajorMinor { return echo.NewHTTPError(http.StatusBadRequest, err) } else if err == compliance.ErrorTailoringNotFound { @@ -193,29 +194,45 @@ func (h *Handlers) GetOscapCustomizationsForPolicy(ctx echo.Context, policy uuid return ctx.JSON(http.StatusOK, cust) } -func (h *Handlers) lintOpenscap(ctx echo.Context, cust *Customizations, fixup bool, distro Distributions, policy string) ([]BlueprintLintItem, error) { - var lintErrors []BlueprintLintItem +func (h *Handlers) lintOpenscap(ctx echo.Context, cust *Customizations, fixup bool, distro Distributions, policy string, savedPolicy *Customizations) ([]BlueprintLintItem, []BlueprintLintItem, error) { var err error d, err := h.server.getDistro(ctx, distro) if err != nil { - return nil, err + return nil, nil, err } major, minor, err := d.RHELMajorMinor() if err != nil { - return nil, err + return nil, nil, err } bp, err := h.server.complianceClient.PolicyCustomizations(ctx.Request().Context(), major, minor, policy) if err == compliance.ErrorTailoringNotFound { - return nil, err + return nil, nil, err } else if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err) + return nil, nil, echo.NewHTTPError(http.StatusInternalServerError, err) } - // make sure all packages are present, all partitions, all enabled/disabled services, all kernel args + var lintErrors []BlueprintLintItem + var lintWarnings []BlueprintLintItem + + // Collect errors and warnings from all lint functions + h.lintPackages(bp, savedPolicy, cust, fixup, &lintErrors, &lintWarnings) + h.lintFilesystems(bp, savedPolicy, cust, fixup, &lintErrors, &lintWarnings) + h.lintServices(bp, savedPolicy, cust, fixup, &lintErrors, &lintWarnings) + h.lintKernel(bp, savedPolicy, cust, fixup, &lintErrors, &lintWarnings) + h.lintFIPS(bp, savedPolicy, cust, fixup, &lintErrors, &lintWarnings) + + return lintErrors, lintWarnings, nil +} + +// lintPackages validates package compliance by performing two comparisons: +// 1. ERRORS: Packages required by current policy but missing from current blueprint +// 2. WARNINGS: Packages that were required by policy in snapshot but no longer required +func (h *Handlers) lintPackages(bp *blueprint.Blueprint, savedPolicy, cust *Customizations, fixup bool, lintErrors *[]BlueprintLintItem, lintWarnings *[]BlueprintLintItem) { + // Check for packages required by current policy but missing from current blueprint (ERRORS) for _, pkg := range bp.GetPackagesEx(false) { if cust.Packages == nil || !slices.Contains(*cust.Packages, pkg) { - lintErrors = append(lintErrors, BlueprintLintItem{ + *lintErrors = append(*lintErrors, BlueprintLintItem{ Name: "Compliance", Description: fmt.Sprintf("package %s required by policy is not present", pkg), }) @@ -224,107 +241,259 @@ func (h *Handlers) lintOpenscap(ctx echo.Context, cust *Customizations, fixup bo } } } - - // some policies (ansi minimal) only require some extra packages - if bp.Customizations == nil { - return lintErrors, nil - } - - for _, fsc := range bp.Customizations.GetFilesystems() { - if cust.Filesystem == nil || !slices.ContainsFunc(*cust.Filesystem, func(fs Filesystem) bool { - return fs.Mountpoint == fsc.Mountpoint - }) { - lintErrors = append(lintErrors, BlueprintLintItem{ - Name: "Compliance", - Description: fmt.Sprintf("mountpoint %s required by policy is not present", fsc.Mountpoint), - }) - if fixup { - cust.Filesystem = common.ToPtr(append(common.FromPtr(cust.Filesystem), Filesystem{ - Mountpoint: fsc.Mountpoint, - MinSize: fsc.MinSize, - })) + // Check for packages in saved policy that are no longer required by current policy (WARNINGS) + if savedPolicy != nil && savedPolicy.Packages != nil { + for _, pkg := range *savedPolicy.Packages { + if !slices.Contains(bp.GetPackagesEx(false), pkg) { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("package %s is no longer required by policy", pkg), + }) } } } +} - if services := bp.Customizations.GetServices(); services != nil { - for _, e := range services.Enabled { - if cust.Services == nil || cust.Services.Enabled == nil || !slices.Contains(*cust.Services.Enabled, e) { - lintErrors = append(lintErrors, BlueprintLintItem{ +// lintFilesystems validates filesystem/mountpoint compliance using the same diffing logic: +// 1. ERRORS: Mountpoints required by current policy but missing from current blueprint +// 2. WARNINGS: Mountpoints that were required in snapshot but no longer needed by current policy +func (h *Handlers) lintFilesystems(bp *blueprint.Blueprint, savedPolicy, cust *Customizations, fixup bool, lintErrors *[]BlueprintLintItem, lintWarnings *[]BlueprintLintItem) { + // Check for filesystems required by current policy but missing from current blueprint (ERRORS) + // Only check if policy actually defines filesystem requirements + if bp.Customizations != nil { + for _, fsc := range bp.Customizations.GetFilesystems() { + if cust.Filesystem == nil || !slices.ContainsFunc(*cust.Filesystem, func(fs Filesystem) bool { return fs.Mountpoint == fsc.Mountpoint }) { + *lintErrors = append(*lintErrors, BlueprintLintItem{ Name: "Compliance", - Description: fmt.Sprintf("service %s required as enabled by policy is not present", e), + Description: fmt.Sprintf("mountpoint %s required by policy is not present", fsc.Mountpoint), }) if fixup { - cust.Services = common.ToPtr(common.FromPtr(cust.Services)) - cust.Services.Enabled = common.ToPtr(append(common.FromPtr(cust.Services.Enabled), e)) + cust.Filesystem = common.ToPtr(append(common.FromPtr(cust.Filesystem), Filesystem{ + Mountpoint: fsc.Mountpoint, + MinSize: fsc.MinSize, + })) } } } - for _, m := range services.Masked { - if cust.Services == nil || cust.Services.Masked == nil || !slices.Contains(*cust.Services.Masked, m) { - lintErrors = append(lintErrors, BlueprintLintItem{ + } + // Check for filesystems in saved policy that are no longer required by current policy (WARNINGS) + if savedPolicy != nil && savedPolicy.Filesystem != nil { + var currentPolicyFilesystems []blueprint.FilesystemCustomization + if bp.Customizations != nil { + currentPolicyFilesystems = bp.Customizations.GetFilesystems() + } + + for _, fs := range *savedPolicy.Filesystem { + if !slices.ContainsFunc(currentPolicyFilesystems, func(fsc blueprint.FilesystemCustomization) bool { return fsc.Mountpoint == fs.Mountpoint }) { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ Name: "Compliance", - Description: fmt.Sprintf("service %s required as masked by policy is not present", m), + Description: fmt.Sprintf("mountpoint %s is no longer required by policy", fs.Mountpoint), }) - if fixup { - cust.Services = common.ToPtr(common.FromPtr(cust.Services)) - cust.Services.Masked = common.ToPtr(append(common.FromPtr(cust.Services.Masked), m)) + } + } + } +} + +// lintServices validates service compliance for enabled/disabled/masked states: +// 1. ERRORS: Services required by current policy but missing from current blueprint +// 2. WARNINGS: Services that were required in snapshot but no longer needed by current policy +func (h *Handlers) lintServices(bp *blueprint.Blueprint, savedPolicy, cust *Customizations, fixup bool, lintErrors *[]BlueprintLintItem, lintWarnings *[]BlueprintLintItem) { + // Check for services required by current policy but missing from current blueprint (ERRORS) + // Only check if policy actually defines service requirements + if bp.Customizations != nil { + if services := bp.Customizations.GetServices(); services != nil { + for _, e := range services.Enabled { + if cust.Services == nil || cust.Services.Enabled == nil || !slices.Contains(*cust.Services.Enabled, e) { + *lintErrors = append(*lintErrors, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("service %s required as enabled by policy is not present", e), + }) + if fixup { + cust.Services = common.ToPtr(common.FromPtr(cust.Services)) + cust.Services.Enabled = common.ToPtr(append(common.FromPtr(cust.Services.Enabled), e)) + } + } + } + for _, m := range services.Masked { + if cust.Services == nil || cust.Services.Masked == nil || !slices.Contains(*cust.Services.Masked, m) { + *lintErrors = append(*lintErrors, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("service %s required as masked by policy is not present", m), + }) + if fixup { + cust.Services = common.ToPtr(common.FromPtr(cust.Services)) + cust.Services.Masked = common.ToPtr(append(common.FromPtr(cust.Services.Masked), m)) + } + } + } + for _, d := range services.Disabled { + if cust.Services == nil || cust.Services.Disabled == nil || !slices.Contains(*cust.Services.Disabled, d) { + *lintErrors = append(*lintErrors, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("service %s required as disabled by policy is not present", d), + }) + if fixup { + cust.Services = common.ToPtr(common.FromPtr(cust.Services)) + cust.Services.Disabled = common.ToPtr(append(common.FromPtr(cust.Services.Disabled), d)) + } } } } - for _, d := range services.Disabled { - if cust.Services == nil || cust.Services.Disabled == nil || !slices.Contains(*cust.Services.Disabled, d) { - lintErrors = append(lintErrors, BlueprintLintItem{ - Name: "Compliance", - Description: fmt.Sprintf("service %s required as disabled by policy is not present", d), - }) - if fixup { - cust.Services = common.ToPtr(common.FromPtr(cust.Services)) - cust.Services.Disabled = common.ToPtr(append(common.FromPtr(cust.Services.Disabled), d)) + } + // Check for services in saved policy that are no longer required by current policy (WARNINGS) + if savedPolicy != nil && savedPolicy.Services != nil { + if savedPolicy.Services.Enabled != nil { + for _, e := range *savedPolicy.Services.Enabled { + // Check if current policy requires this enabled service + stillRequired := false + if bp.Customizations != nil { + if policyServices := bp.Customizations.GetServices(); policyServices != nil { + stillRequired = slices.Contains(policyServices.Enabled, e) + } + } + if !stillRequired { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("service %s is no longer required as enabled by policy", e), + }) + } + } + } + if savedPolicy.Services.Disabled != nil { + for _, d := range *savedPolicy.Services.Disabled { + // Check if current policy requires this disabled service + stillRequired := false + if bp.Customizations != nil { + if policyServices := bp.Customizations.GetServices(); policyServices != nil { + stillRequired = slices.Contains(policyServices.Disabled, d) + } + } + if !stillRequired { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("service %s is no longer required as disabled by policy", d), + }) + } + } + } + if savedPolicy.Services.Masked != nil { + for _, m := range *savedPolicy.Services.Masked { + // Check if current policy requires this masked service + stillRequired := false + if bp.Customizations != nil { + if policyServices := bp.Customizations.GetServices(); policyServices != nil { + stillRequired = slices.Contains(policyServices.Masked, m) + } + } + if !stillRequired { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("service %s is no longer required as masked by policy", m), + }) } } } } +} - if kernel := bp.Customizations.Kernel; kernel != nil { - if kernel.Name != "" && (cust.Kernel == nil || *cust.Kernel.Name != kernel.Name) { - lintErrors = append(lintErrors, BlueprintLintItem{ - Name: "Compliance", - Description: fmt.Sprintf("kernel name %s required by policy not set", kernel.Name), - }) - if fixup { - cust.Kernel = common.ToPtr(common.FromPtr(cust.Kernel)) - cust.Kernel.Name = common.ToPtr(kernel.Name) +// lintKernel validates kernel settings (name and command line parameters): +// 1. ERRORS: Kernel settings required by current policy but missing from current blueprint +// 2. WARNINGS: Kernel settings that were required in snapshot but no longer needed by current policy +func (h *Handlers) lintKernel(bp *blueprint.Blueprint, savedPolicy, cust *Customizations, fixup bool, lintErrors *[]BlueprintLintItem, lintWarnings *[]BlueprintLintItem) { + // Check for kernel settings required by current policy but missing from current blueprint (ERRORS) + // Only check if policy actually defines kernel requirements + if bp.Customizations != nil { + if kernel := bp.Customizations.Kernel; kernel != nil { + if kernel.Name != "" { + if cust.Kernel == nil || cust.Kernel.Name == nil || *cust.Kernel.Name != kernel.Name { + *lintErrors = append(*lintErrors, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("kernel name %s required by policy not set", kernel.Name), + }) + if fixup { + cust.Kernel = common.ToPtr(common.FromPtr(cust.Kernel)) + cust.Kernel.Name = common.ToPtr(kernel.Name) + } + } + } + for _, kcmd := range strings.Fields(kernel.Append) { + if kcmd == "" { + continue + } + if cust.Kernel == nil || cust.Kernel.Append == nil || !strings.Contains(*cust.Kernel.Append, kcmd) { + *lintErrors = append(*lintErrors, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("kernel command line parameter '%s' required by policy not set", kcmd), + }) + if fixup { + cust.Kernel = common.ToPtr(common.FromPtr(cust.Kernel)) + cust.Kernel.Append = common.ToPtr(fmt.Sprintf("%s %s", common.FromPtr(cust.Kernel.Append), kcmd)) + } + } } } - kernelcmd := strings.Split(kernel.Append, " ") - for _, kcmd := range kernelcmd { - if cust.Kernel == nil || !strings.Contains(*cust.Kernel.Append, kcmd) { - lintErrors = append(lintErrors, BlueprintLintItem{ + } + // Check for kernel settings in saved policy that are no longer required by current policy (WARNINGS) + if savedPolicy != nil && savedPolicy.Kernel != nil { + var policyKernel *blueprint.KernelCustomization + if bp.Customizations != nil { + policyKernel = bp.Customizations.Kernel + } + if savedPolicy.Kernel.Name != nil { + if policyKernel == nil || policyKernel.Name != *savedPolicy.Kernel.Name { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ Name: "Compliance", - Description: fmt.Sprintf("kernel command line parameter '%s' required by policy not set", kcmd), + Description: fmt.Sprintf("kernel name %s is no longer required by policy", *savedPolicy.Kernel.Name), }) } - if fixup { - cust.Kernel = common.ToPtr(common.FromPtr(cust.Kernel)) - cust.Kernel.Append = common.ToPtr(fmt.Sprintf("%s %s", common.FromPtr(cust.Kernel.Append), kcmd)) + } + if savedPolicy.Kernel.Append != nil { + for _, kcmd := range strings.Fields(*savedPolicy.Kernel.Append) { + if kcmd == "" { + continue + } + if policyKernel == nil || !strings.Contains(policyKernel.Append, kcmd) { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("kernel command line parameter '%s' is no longer required by policy", kcmd), + }) + } } } } +} - if fips := bp.Customizations.FIPS; fips != nil { - if *fips && (cust.Fips == nil || cust.Fips.Enabled == nil) { - lintErrors = append(lintErrors, BlueprintLintItem{ - Name: "Compliance", - Description: fmt.Sprintf("FIPS required '%t' by policy but not set", *fips), - }) - if fixup { - cust.Fips = &FIPS{ - Enabled: fips, +// lintFIPS validates FIPS compliance settings: +// 1. ERRORS: FIPS required by current policy but not enabled in current blueprint +// 2. WARNINGS: FIPS was required in snapshot but no longer needed by current policy +func (h *Handlers) lintFIPS(bp *blueprint.Blueprint, savedPolicy, cust *Customizations, fixup bool, lintErrors *[]BlueprintLintItem, lintWarnings *[]BlueprintLintItem) { + // Check for FIPS required by current policy but not set in current blueprint (ERRORS) + // Only check if policy actually defines FIPS requirements + if bp.Customizations != nil { + if fips := bp.Customizations.FIPS; fips != nil { + if cust.Fips == nil || cust.Fips.Enabled == nil || !*cust.Fips.Enabled { + if fixup { + cust.Fips = &FIPS{Enabled: fips} + } else { + *lintErrors = append(*lintErrors, BlueprintLintItem{ + Name: "Compliance", + Description: fmt.Sprintf("FIPS required '%t' by policy but not set", *fips), + }) } } } } - - return lintErrors, nil + // Check for FIPS in saved policy that is no longer required by current policy (WARNINGS) + if savedPolicy != nil && savedPolicy.Fips != nil && savedPolicy.Fips.Enabled != nil && *savedPolicy.Fips.Enabled { + var policyFips *bool + if bp.Customizations != nil { + policyFips = bp.Customizations.FIPS + } + if policyFips == nil || !*policyFips { + *lintWarnings = append(*lintWarnings, BlueprintLintItem{ + Name: "Compliance", + Description: "FIPS is no longer required by policy", + }) + } + } }