Skip to content

[Security Solution][Detection Engine] Indicator match rule reports max alerts warning but creates fewer alerts #259169

@marshallmain

Description

@marshallmain

In the source event first implementation for IM rules, there's a bug with the logic that counts created alerts. Here if the page of source documents matches no indicators, we short circuit and return currentResult from the function. currentResult contains the data about rule execution so far - including the number of alerts created from previous pages. However, the function is only supposed to return information about what happened for the current page, e.g. how many alerts were created from the current page of source docs. Because of this, every page that generates no alerts will double the alert count in the higher level result tracking without actually creating more alerts.

Since the paging logic will break out of the loop if the (incorrect) count hits max signals, the warning is still accurate: some alerts may have been missed.

Luckily, the fix is simple. When no alerts are created, and in error cases, we need to return an object from createEventSignal that accurately represents that no alerts were created for that page rather than returning currentResult. It appears that we should not pass currentResult into createEventSignal at all since the function does not need any information about the overall results - this will help clarify the logical model of the functions, i.e. that createEventSignal should deal with one page of source docs in isolation and return information about that specific page.

Repro steps

@nkhristinin created an integration test to reproduce the issue. Add the integration test in x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match.ts.

Details
// Reproduces createdSignalsCount inflation when no-op chunks return currentResult.
    // We seed 2 matching events followed by 10 non-matching events and force 1 event/page.
    // The first two pages create 2 alerts; later no-op pages inflate the counter and can
    // trigger a false max-signals warning despite only 2 created preview alerts.
    it.only('reproduces false max alerts warning when later event pages have no threat matches', async () => {
      const id = `repro${Date.now()}`;
      const baseTs = moment();
      const timestamp = baseTs.toISOString();

      const matchingEvents = [
        {
          id,
          user: { name: 'matchuser' },
          '@timestamp': baseTs.clone().subtract(1, 's').toISOString(),
          'event.ingested': baseTs.clone().subtract(1, 's').toISOString(),
        },
        {
          id,
          user: { name: 'matchuser' },
          '@timestamp': baseTs.clone().subtract(2, 's').toISOString(),
          'event.ingested': baseTs.clone().subtract(2, 's').toISOString(),
        },
      ];
      const nonMatchingEvents = Array.from({ length: 10 }, (_, i) => ({
        id,
        user: { name: `eventmiss${i + 1}` },
        '@timestamp': baseTs
          .clone()
          .subtract(i + 3, 's')
          .toISOString(),
        'event.ingested': baseTs
          .clone()
          .subtract(i + 3, 's')
          .toISOString(),
      }));
      const threats = [
        {
          ...threatDoc(id, timestamp),
          user: { name: 'matchuser' },
        },
        ...Array.from({ length: 19 }, (_, i) => ({
          ...threatDoc(
            id,
            baseTs
              .clone()
              .subtract(i + 1, 'm')
              .toISOString()
          ),
          user: { name: `threatfiller${i + 1}` },
        })),
      ];

      await indexListOfDocuments([...matchingEvents, ...nonMatchingEvents, ...threats]);

      const rule: ThreatMatchRuleCreateProps = {
        ...threatMatchRuleEcsComplaint(id),
        threat_mapping: [
          {
            entries: [{ field: 'user.name', value: 'user.name', type: 'mapping' }],
          },
        ],
        items_per_search: 1,
        concurrent_searches: 1,
      };

      const { logs, previewId } = await previewRule({ supertest, rule });
      const previewAlerts = await getPreviewAlerts({ es, previewId, size: 1000 });
      console.log('previwaAlerts length ---- ');
      console.log(previewAlerts.length);
      const allWarnings = logs.flatMap((l) => l.warnings ?? []);

      // Exact created alert count can vary with fixture composition, but should remain small
      // while the max alerts warning is still emitted due to inflated createdSignalsCount.
      expect(previewAlerts.length).toBeGreaterThan(0);
      expect(previewAlerts.length).toBeLessThan(100);
      expect(allWarnings).not.toContain(getMaxAlertsWarning());
    });

Metadata

Metadata

Assignees

Labels

Feature:Indicator Match RuleSecurity Solution Indicator Match rule typeTeam:Detection EngineSecurity Solution Detection Engine AreabugFixes for quality problems that affect the customer experienceimpact:highAddressing this issue will have a high level of impact on the quality/strength of our product.sdh-linkedv9.2.8v9.3.3v9.4.0

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions