Skip to content

refactor(engine): remove direct db dependency from actions logic#6305

Open
sachin9058 wants to merge 1 commit intomindersec:mainfrom
sachin9058:refactor/engine-actions-remove-db-dependency
Open

refactor(engine): remove direct db dependency from actions logic#6305
sachin9058 wants to merge 1 commit intomindersec:mainfrom
sachin9058:refactor/engine-actions-remove-db-dependency

Conversation

@sachin9058
Copy link
Copy Markdown
Contributor

@sachin9058 sachin9058 commented Apr 8, 2026

Summary

This PR builds on the recently merged #6289 and further simplifies the engine layer by removing remaining direct dependencies on internal/db types from the actions logic.

Previously, the engine layer (internal/engine/actions) relied on database-specific types such as db.ListRuleEvaluationsByProfileIdRow, creating tight coupling between business logic and persistence.

While #6289 introduced an adapter-based approach for mapping database types, this PR refines the design by:

  • Introducing engine-level abstractions (EvalStatus, PreviousEval)
  • Refactoring action decision logic (shouldRemediate, shouldAlert) to operate solely on these types
  • Performing conversion at the boundary (DoActions) instead of relying on adapter mappings

Additionally, this PR fixes incorrect handling of evaluation states by properly distinguishing between:

  • evaluation failures
  • skipped evaluations
  • generic errors

This ensures correct action behavior (e.g., avoiding unintended alerts or remediation on generic errors).

Changes

  • Removed direct usage of internal/db types from engine logic
  • Introduced engine-level domain types (EvalStatus, PreviousEval)
  • Refactored remediation and alert decision logic to use engine abstractions
  • Updated tests to align with the new types
  • Corrected behavior for error vs failure handling

Benefits

  • Improves separation of concerns between engine and persistence layers
  • Simplifies architecture by reducing indirection
  • Avoids import cycle risks introduced by adapter-based approaches
  • Improves correctness of action decisions
  • Enhances testability and maintainability

No new dependencies are introduced.


Testing

The changes were validated using the following steps:

Build Verification

go build ./...

Linting

golangci-lint run

Unit Tests

go test ./...
  • Updated actions_test.go to use engine-level abstractions

  • Verified correct behavior across:

    • success, failure, skipped, and error scenarios
    • nil previous evaluation cases
    • remediation and alert transitions

Manual Verification

Validated that:

  • errors no longer incorrectly trigger remediation/alert actions
  • logic behaves consistently with expected evaluation outcomes

All checks pass successfully with no lint issues or test failures.


Fixes #NONE

@sachin9058 sachin9058 requested a review from a team as a code owner April 8, 2026 16:05
@sachin9058
Copy link
Copy Markdown
Contributor Author

Open to feedback if a different approach is preferred.

@sachin9058 sachin9058 force-pushed the refactor/engine-actions-remove-db-dependency branch 3 times, most recently from 589de8f to 8e9d9d1 Compare April 8, 2026 17:38
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 8, 2026

Coverage Status

coverage: 59.368% (-0.02%) from 59.39% — sachin9058:refactor/engine-actions-remove-db-dependency into mindersec:main

Copy link
Copy Markdown
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it is making good progress. A couple small comments (your LLM is still rewriting a lot of comments it doesn't need to), but this is looking promising.

Comment thread internal/engine/actions/actions.go Outdated
Comment on lines +113 to +121
var prev *PreviousEval
if row := params.GetEvalStatusFromDb(); row != nil {
prev = &PreviousEval{
RemediationStatus: RemediationStatus(row.RemStatus),
AlertStatus: AlertStatus(row.AlertStatus),
RemediationMeta: &row.RemMetadata,
AlertMeta: &row.AlertMetadata,
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we construct prev once for both remediate and alert?

Comment thread internal/engine/actions/actions.go Outdated
Msg("panic in action execution")
finalErr = enginerr.ErrInternal

err = enginerr.ErrInternal // restore behavior
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment makes sense. LLM leftover?

Comment thread internal/engine/actions/actions.go Outdated
// previous remediation state.
func shouldRemediate(prevEval *PreviousEval, evalStatus EvalStatus) engif.ActionCmd {
// Determine current evaluation status
newEval := evalStatus
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do this rename?

Comment thread internal/engine/actions/actions.go
Comment thread internal/engine/actions/actions.go Outdated
Comment on lines +217 to +218
// Determine current evaluation status
newEval := evalStatus
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I think we can just rename the evalStatus argument to newEval and avoid this assignment.

Comment thread internal/engine/actions/actions.go Outdated
Comment on lines +201 to +226
// Case 1 - Successful remediation of a type that is not PR is considered instant.
// Case: successful remediation (non-PR type)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original comment explained that non-PR remediations were "instant", so they would already be fixed. That's useful context for why this case is what it is.

Comment on lines -216 to -219
// Case 3 - Evaluation changed from something else to ERROR -> Alert should be ON
// Case 4 - Evaluation has changed from something else to FAILED -> Alert should be ON
// The Alert should be on (if it wasn't already)
if db.AlertStatusTypesOn != prevAlert {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep these "Case X" statements? It helps understand the original intent of the code -- unless you feel like you've substantially adjusted the logic here on purpose.

Comment thread internal/engine/actions/actions.go Outdated
if err != nil {
logger.Error().Err(err).Msg("error marshaling empty json.RawMessage")
logger.Error().Err(err).Msg("error marshaling empty json")
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just replace this with a direct assignment, and avoid the need for error handling.

Suggested change
}
// Even though meta is an empty json struct by default, there's no risk of overwriting
// any existing meta entry since we don't upsert in case of conflict while skipping
m := json.RawMessage("{}")

@sachin9058 sachin9058 force-pushed the refactor/engine-actions-remove-db-dependency branch from 71f4d58 to 24ffab7 Compare April 8, 2026 22:45
@sachin9058
Copy link
Copy Markdown
Contributor Author

@evankanderson Thanks for the feedback !!! really helpful! I’ve gone through and addressed the points you mentioned, including cleaning up some leftover comments, reusing prev, and restoring the original case comments to keep the intent clear.

Happy to tweak anything further if needed.

Copy link
Copy Markdown
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still a lot of comment movement; it makes it somewhat harder to review the diff (since about 25% of it is no-op rewordings or addition of "." to the end of sentences). I can work around it, but it's generally helpful to practice producing minimal diffs when there's another human reviewing the code.

Comment on lines -158 to -159
// Case 2 - Evaluation changed from something else to ERROR -> Remediation should be OFF
// Case 3 - Evaluation changed from something else to PASSING -> Remediation should be OFF
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're still deleting these "Case N" statements. Are you sure that the code is matching the comments? If not, I'd leave in the (conflicting) comments until we figure out whether the code or the comments are correct.

Comment on lines -167 to -168
// Case 4 - Evaluation has changed from something else to FAILED -> Remediation should be ON
// We should remediate only if the previous remediation was skipped, so we don't risk endless remediation loops
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And keep these comments, please.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Keep this comment about endless remediation loops, too)


// Case 1 - Successful remediation of a type that is not PR is considered instant.
if remType != pull_request.RemediateType && remErr == nil {
// If this is the case either skip alerting or turn it off if it was on
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I think this comment helps clarify the intent beyond simply what the code does.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this comment about skipping alerting or turning it off -- this code is more subtle than I'd like (it tries to interpret past and present to "understand" what edge-change is happening, rather than simply copying status from A to B).

Comment on lines -225 to -226
// Case 5 - Evaluation changed from something else to PASSING -> Alert should be OFF
// The Alert should be turned OFF (if it wasn't already)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, keep this comment if you can. If you can keep the indentation the same, it will help GitHub line up the diff correctly (right now, it's anchoring on blank lines stupidly, because it can't figure out which lines are common).

Comment thread internal/engine/actions/actions.go Outdated
// If the action is not found, definitely skip it
logger.Msg("action type not found, skipping")
zerolog.Ctx(ctx).Info().
Str("eval_status", fmt.Sprintf("%v", evalErr)).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If evalErr is an error, you can simply call evalErr.Error() to get the string representation.

logger.Msg("unknown action option, skipping")
return true
case models.ActionOptDryRun, models.ActionOptOn:
// Action is on or dry-run, do not skip yet. Check the evaluation error
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this error helps to explain why this is not simply false or true. Can you leave at least this comment?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep some comment about some on and dry-run actions still get skipped because the evaluation requested a "skip" result.

Comment on lines +284 to 290
// getAlertMeta returns alert metadata from previous evaluation.
func getAlertMeta(prevEval *PreviousEval) *json.RawMessage {
if prevEval != nil {
return prevEval.AlertMeta
}
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that PreviousEval is a struct we define here (rather than a DB row), we can define a GetAlertMeta() *json.RawMessage function that will do this even if the pointer is nil, rather than needing a free function:

Suggested change
// getAlertMeta returns alert metadata from previous evaluation.
func getAlertMeta(prevEval *PreviousEval) *json.RawMessage {
if prevEval != nil {
return prevEval.AlertMeta
}
return nil
}
// getAlertMeta returns alert metadata from previous evaluation.
func (p *PreviousEval) GetAlertMeta() *json.RawMessage {
if p!= nil {
return p.AlertMeta
}
return nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also not clear to me that PreviousEval needs to be exported, so it might be spelled previousEval...

Comment thread internal/engine/actions/actions_test.go Outdated
Comment on lines +222 to +231
@@ -213,7 +223,17 @@ func TestShouldAlert(t *testing.T) {
if !tt.nilRow {
prevRow = &db.ListRuleEvaluationsByProfileIdRow{AlertStatus: tt.prevAlert}
}
got := shouldAlert(prevRow, tt.evalErr, tt.remErr, tt.remType)
var prev *PreviousEval
if prevRow != nil {
prev = &PreviousEval{
AlertStatus: AlertStatus(prevRow.AlertStatus),
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to have prevRow be a DB type anymore, right? It could just be a *PreviousEval?

Suggested change
}
var prev *PreviousEval
if !tt.nilRow {
prev = &PreviousEval{
AlertStatus: AlertStatus(tt.prevAlert),
}
}

(Some of the rows are outside the suggestion window, so it presents this like a comment on a single line.

@sachin9058 sachin9058 force-pushed the refactor/engine-actions-remove-db-dependency branch from f09e0c6 to af3b673 Compare April 10, 2026 20:47
@sachin9058
Copy link
Copy Markdown
Contributor Author

@evankanderson
Thanks for the feedback!

I've addressed the review comments:

  • restored the original "Case X" comments to preserve intent
  • removed unnecessary comment rewrites to keep the diff minimal
  • added the TODO for EvalStatusError as suggested
  • fixed logging to use evalErr.Error()

I also rebased the branch on the latest main and resolved conflicts.

Please let me know if anything else should be adjusted.

Copy link
Copy Markdown
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One lint error, and a bit more return of the comments, and this should be good to go.

"github.com/rs/zerolog"
"google.golang.org/protobuf/reflect/protoreflect"

"strings"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"strings" should be in the block with "time" and "fmt", as it's part of the go standard library (and the linter is complaining)

if prevRemediation != RemediationStatus("skipped") {
return engif.ActionCmdOff
}
// We should do nothing if remediation was already skipped
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment again is helpful explaining why we return "Do Nothing" when the comment says " -> OFF". It might have been better to store the previous state On/Off state, but that's not what's going on here, so it's useful to understand the intents.

Comment on lines -167 to -168
// Case 4 - Evaluation has changed from something else to FAILED -> Remediation should be ON
// We should remediate only if the previous remediation was skipped, so we don't risk endless remediation loops
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Keep this comment about endless remediation loops, too)


// Case 1 - Successful remediation of a type that is not PR is considered instant.
if remType != pull_request.RemediateType && remErr == nil {
// If this is the case either skip alerting or turn it off if it was on
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this comment about skipping alerting or turning it off -- this code is more subtle than I'd like (it tries to interpret past and present to "understand" what edge-change is happening, rather than simply copying status from A to B).

logger.Msg("unknown action option, skipping")
return true
case models.ActionOptDryRun, models.ActionOptOn:
// Action is on or dry-run, do not skip yet. Check the evaluation error
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep some comment about some on and dry-run actions still get skipped because the evaluation requested a "skip" result.

@sachin9058 sachin9058 force-pushed the refactor/engine-actions-remove-db-dependency branch from af3b673 to 8781cf7 Compare April 11, 2026 13:44
@sachin9058
Copy link
Copy Markdown
Contributor Author

@evankanderson Thanks for the detailed feedback, that clarified the intent a lot.

I’ve updated the comments to better reflect the reasoning behind remediation decisions and alert transitions, especially around avoiding repeated remediation and handling state changes correctly. I also addressed the lint issues and rebased on the latest upstream.

Happy to make further adjustments if needed.

Copy link
Copy Markdown
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to revert the changes to entities/properties/service from this PR.

(In general, it's good ta take a look at the actual file diff to make sure that the contents roughly match what you expect the reviewer to be looking at.)

Comment on lines +229 to +230
// handle duplicate key error gracefully (acts like update)
if strings.Contains(err.Error(), "duplicate key") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this here (why is it part of this PR, at least)?

Secondarily (meaning: if you are opening another PR for this), It looks like UpsertPropertyValueV1 bottoms out in:

INSERT INTO properties (
    entity_id,
    key,
    value,
    updated_at
) VALUES ($1, $2, $3, NOW())
ON CONFLICT (entity_id, key) DO UPDATE
    SET
        value = $4,
        updated_at = NOW()
RETURNING id, entity_id, key, value, updated_at

Which should already handle the duplicate key case. If we get an error, it seems like we probably have some other problem that prevented this.

@sachin9058 sachin9058 force-pushed the refactor/engine-actions-remove-db-dependency branch from 1e2a2a0 to 22c3fbc Compare April 13, 2026 09:44
@sachin9058
Copy link
Copy Markdown
Contributor Author

@evankanderson
I've cleaned up the branch and squashed the changes into a single commit so the PR only includes the intended engine refactor.

It now only contains the actions logic updates along with the new internal types to decouple from database representations.

I've also addressed the previous feedback around comments and structure.

Happy to refine further if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants