docs/ref/modules/sca/architecture.md
The SCA module implements a security configuration assessment system that evaluates system compliance against security policies. The module loads these policies from YAML files, executes security checks, and synchronizes results with the manager.
The central C++ implementation (SecurityConfigurationAssessment class) that coordinates SCA operations.
Key responsibilities:
SCAPolicyLoader)Handles loading and parsing of YAML policy files.
Key responsibilities:
Individual evaluators for different rule types found in policy checks.
Available evaluators:
Each evaluator processes specific rule syntax and returns pass/fail results.
SCAEventHandler)Manages the generation and delivery of SCA events.
Key responsibilities:
Uses the shared DBSync component for local SQLite database operations.
Key responsibilities:
sca_policy tablesca_check tableIntegrates with Wazuh's Agent Sync Protocol for reliable manager communication.
Key responsibilities:
wm_sca_main() loads the SCA dynamic library and sets up callbackssca_start() initializes the SCA implementation with configurationSecurityConfigurationAssessment::Setup callbacks and other configuration values.SecurityConfigurationAssessment::Run Starts the execution flowSecurityConfigurationAssessment::Run() executes the main loopSCAPolicyLoader loads enabled policies from YAML filesSCAEventHandler compares results with stored stateThe SCA module includes automatic recovery to detect and resolve synchronization inconsistencies:
integrity_interval elapses, an integrity check is performedsca_check tablerequiresFullSync() in Agent Sync Protocollast_integrity_check timestamp is stored in sca_metadata tableSync Thread (wm_sca_sync_module)
│
▼
DELTA Sync
│
▼
Check if integrity_interval elapsed
│
├─► No → Skip integrity check
│
└─► Yes → Calculate table checksum
│
▼
Send checksum to manager
│
├─► Match → No action needed
│
└─► Mismatch → Perform full recovery
│
├─► Load all checks from DB
├─► Clear in-memory sync data
├─► Rebuild stateful messages
└─► Trigger FULL synchronization
The SCA module operates with the following threads:
wm_sca_main): Runs the SCA implementation and handles policy executionwm_sca_sync_module): Handles periodic synchronization with the manager (when enabled)When the SCA module is disabled, the wm_handle_sca_disable_and_notify_data_clean() function executes a cleanup procedure to notify the manager and remove local databases. This ensures the manager's state remains synchronized with the agent's actual module status.
The function is called during module startup in wm_sca_main() when data->enabled is false:
if (data->enabled) {
minfo("SCA module enabled.");
} else {
wm_handle_sca_disable_and_notify_data_clean();
minfo("SCA module disabled. Exiting.");
pthread_exit(NULL);
}
Module Startup (wm_sca_main)
│
▼
Check data->enabled
│
▼ (if disabled)
wm_handle_sca_disable_and_notify_data_clean()
│
├─► Check for SCA database file ───► w_is_file(SCA_DB_DISK_PATH)
│ │
│ ├─► File exists
│ │ (proceed with cleanup)
│ │
│ └─► File not exists
│ (skip notification, exit)
│
Load SCA module dynamically
│
▼
Configure SCA module minimally
│
├─► Set logging callback ─────────► sca_set_log_function(sca_log_callback)
│
├─► Set sync parameters ──────────► sca_set_sync_parameters()
│ (module name, DB path, MQ funcs)
│
└─► Initialize module ─────────────► sca_init()
│
▼
Send data clean notification ────────► sca_notify_data_clean()
│ │
│ ├─► indices: [SCA_SYNC_INDEX]
│ ├─► Retry on failure
│ │ (wait sca_sync_interval)
│ │
│ └─► Success confirmation
│
└─► Delete databases ─────────► sca_delete_database()
1. Agent starts with SCA module disabled (enabled = false)
2. SCA database file exists at SCA_DB_DISK_PATH
3. Load SCA module dynamically
4. Configure logging and sync parameters
5. Initialize SCA module structures
6. Send data clean notification to manager (with infinite retries)
7. Manager removes SCA_SYNC_INDEX from agent's state
8. Delete sync protocol database
9. Exit module startup
1. Agent starts with SCA module disabled (enabled = false)
2. SCA database file does not exist
3. Skip data clean notification (nothing to clean)
4. Exit module startup immediately
When all SCA policies are removed from configuration while the module remains enabled, the module detects this at startup or during runtime and initiates a cleanup process.
| Scenario | Action |
|---|---|
| All policies removed | DataClean sent to manager → DB tables cleared → module exits |
| Partial removal | DBSync generates delete events → module continues with remaining policies |
Policy Loading
│
▼
No enabled policies found?
│
├─► No → Continue normal scanning
│
└─► Yes → Check if DB has existing data
│
├─► No data → Exit (nothing to clean)
│
└─► Has data → Send DataClean notification
│
├─► Clear sca_policy table
├─► Clear sca_check table
└─► Exit module
The coordination commands provide external control over SCA operations, allowing the agent-info to coordinate module behavior. SCA implements a synchronous blocking model using C++ threading primitives, similar to Syscollector.
Purpose: Allow temporary suspension of SCA scanning operations without stopping the module completely.
Implementation:
The pause command follows this synchronous sequence:
Pause Command Received
│
▼
Set m_paused = true (atomic flag)
│
▼
Wait for Current Operations
│
├─► Wait for m_scanInProgress = false
│ (any ongoing scan completes)
│
└─► Wait for m_syncInProgress = false
(any ongoing sync completes)
│
▼
Both Operations Complete
│
▼
Return to Caller (blocking complete)
The resume command:
Resume Command Received
│
▼
Set m_paused = false (atomic flag)
│
▼
Notify Main Loop (m_cv.notify_one())
│
▼
SCA Resumes Normal Operations
Purpose: Force immediate synchronization of pending SCA check results, bypassing the normal sync interval.
Implementation:
Flush Command Received
│
▼
Check if Sync Protocol Initialized
│
├─► Not Initialized → Return 0 (nothing to flush)
│
└─► Initialized
│
▼
Call synchronizeModule(Mode::DELTA)
│
├─► Waits for manager acknowledgment
└─► Returns sync result
Purpose: Query and set version numbers for tracking SCA scanning operations and coordination state.
getMaxVersion() Implementation:
getMaxVersion() Called
│
▼
Execute SQL Query
│
└─► SELECT MAX(version) FROM sca_check
│
▼
Return Maximum Version (or 0 if empty, -1 on error)
setVersion() Implementation:
setVersion(newVersion) Called
│
▼
Start Database Transaction
│
├─► For Each Row in sca_check Table:
│ │
│ ├─► Retrieve all columns
│ │
│ ├─► Update version = newVersion
│ │
│ └─► Continue to next row
│
▼
Commit Transaction
│
▼
Return 0 (success) or -1 (error)
SCA integrates with the Schema Validator module to ensure all security check results conform to the expected Wazuh indexer schema before transmission.
Schema validation occurs at three critical points in the SCA lifecycle:
When policies or checks are modified, validation occurs before sending to the sync protocol:
const bool validationPassed = ValidateAndHandleStatefulMessage(
processedStatefulEvent,
"policy/check event",
checkDataForDelete,
&failedChecks
);
if (validationPassed)
{
PushStateful(processedStatefulEvent, operation, version);
}
Key characteristics:
failedChecks vectorWhen individual check results are reported:
const bool validationPassed = ValidateAndHandleStatefulMessage(
stateful,
"checkId: " + checkId,
dataForDelete,
&failedChecks
);
if (validationPassed)
{
PushStateful(stateful, operation, version);
}
// Delete checks that failed schema validation (batch delete with transaction)
DeleteFailedChecksFromDB(failedChecks);
Key characteristics:
When performing integrity recovery, only valid items are synchronized:
auto validationResult = validator->validate(statefulMessage.dump());
if (!validationResult.isValid)
{
// Log validation errors
std::string errorMsg = "Schema validation failed for SCA recovery message (policy: " +
policyId + ", check: " + checkId + ", index: " +
std::string(SCA_SYNC_INDEX) + "). Errors: ";
for (const auto& error : validationResult.errors)
{
errorMsg += " - " + error;
}
LoggingHelper::getInstance().log(LOG_ERROR, errorMsg);
LoggingHelper::getInstance().log(LOG_ERROR, "Raw recovery event that failed validation: " +
statefulMessage.dump());
LoggingHelper::getInstance().log(LOG_DEBUG, "Skipping persistence of invalid recovery event");
shouldPersist = false;
}
Key characteristics:
The SCA Event Handler provides two helper functions:
Validates a stateful SCA message and marks it for deletion if validation fails:
bool ValidateAndHandleStatefulMessage(const nlohmann::json& statefulEvent,
const std::string& context,
const nlohmann::json& checkData,
std::vector<nlohmann::json>* failedChecks) const;
Behavior:
true if validation passed or validator not initializedfalse if validation failedfailedChecks vector for deferred deletionDeletes failed checks from DBSync in a batch transaction:
void DeleteFailedChecksFromDB(const std::vector<nlohmann::json>& failedChecks) const;
Implementation:
void SCAEventHandler::DeleteFailedChecksFromDB(
const std::vector<nlohmann::json>& failedChecks) const
{
if (failedChecks.empty() || !m_dBSync)
{
return;
}
try
{
DBSyncTxn deleteTxn(m_dBSync->handle(), nlohmann::json::array(), 0, 1,
[](ReturnTypeCallback, const nlohmann::json&) {});
for (const auto& failedCheck : failedChecks)
{
auto deleteQuery = DeleteQuery::builder()
.table("sca_check")
.data(failedCheck)
.build();
m_dBSync->deleteRows(deleteQuery.query());
}
deleteTxn.getDeletedRows([](ReturnTypeCallback, const nlohmann::json&) {});
LoggingHelper::getInstance().log(LOG_DEBUG, "Deleted " +
std::to_string(failedChecks.size()) +
" SCA check(s) from DBSync due to validation failure");
}
catch (const std::exception& e)
{
LoggingHelper::getInstance().log(LOG_ERROR, "Failed to delete from DBSync: " +
std::string(e.what()));
}
}
Behavior:
SCA uses a deferred deletion pattern to safely remove invalid checks:
Flow:
1. Start Event Processing
│
├─► Create failedChecks vector
│
▼
2. Process Events
│
├─► For each policy/check event:
│ │
│ ├─► Validate against schema
│ │
│ ├─► If validation fails:
│ │ │
│ │ ├─► Log error
│ │ │
│ │ └─► Add to failedChecks vector
│ │
│ └─► If validation passes:
│ │
│ └─► Push to stateful queue
│
▼
3. After All Events
│
├─► DeleteFailedChecksFromDB(failedChecks)
│ │
│ ├─► Create DBSync transaction
│ │
│ ├─► Delete all failed checks
│ │
│ └─► Commit transaction
│
▼
4. Complete
Why Deferred?
SCA validates data against the wazuh-states-sca index:
Schema Structure:
check.*: Check details (id, name, description, result, etc.)policy.*: Policy metadata (id, name, file, references)state.*: State information (modified_at, document_version)checksum.*: Integrity checksumWhen a check result is reported but the check is not found in the database, SCA logs a debug message:
LoggingHelper::getInstance().log(LOG_DEBUG,
"Check " + checkId + " not found in DB (may have been deleted due to validation failure), skipping report");
Previous Behavior: Logged as LOG_WARNING without explanation
New Behavior:
LOG_DEBUG level (expected behavior, not an error)Occurs when:
Initialization:
[INFO] Schema validator initialized successfully from embedded resources
Validation Failure:
[ERROR] Schema validation failed for SCA message (checkId: cis_rhel7_1.1.1, index: wazuh-states-sca). Errors:
- Field 'check.result' expected type 'keyword', got 'integer'
[ERROR] Raw event that failed validation: {"check":{"id":"cis_rhel7_1.1.1","result":1}}
[DEBUG] Marking SCA check for deferred deletion due to validation failure
Batch Deletion:
[DEBUG] Deleted 2 SCA check(s) from DBSync due to validation failure
Check Not Found:
[DEBUG] Check cis_rhel7_1.1.1 not found in DB (may have been deleted due to validation failure), skipping report
Graceful Degradation:
If the schema validator is not initialized:
Integration points:
sca_impl.cpp)sca_event_handler.cpp)sca_event_handler.cpp)sca_impl.cpp)Stores individual security checks:
id: Check ID (primary key)policy_id: Foreign key to sca_policyname: Check namedescription: Check descriptionresult: Check result (Passed, Failed, Not applicable, Not run)reason: Reason for result (optional)version: Document version for synchronization