Skip to content

Conversation

@abhishekbhatia1710
Copy link
Contributor

@abhishekbhatia1710 abhishekbhatia1710 commented Nov 5, 2025

Summary

  • add the threat hunting home page and supporting components for the entity analytics area when the entityThreatHuntingEnabled flag is on
  • wire the new page into entity analytics navigation/routes and surface new table actions for entity timelines and AI assistant entry points
  • provide focused unit coverage for the new home experience

Screenshot of the UI :

image image

@abhishekbhatia1710 abhishekbhatia1710 requested review from a team as code owners November 5, 2025 10:45
@abhishekbhatia1710 abhishekbhatia1710 self-assigned this Nov 5, 2025
@abhishekbhatia1710 abhishekbhatia1710 changed the title [Security Solution] Introduce threat hunting home experience [Security Solution][Entity Analytics][Threat Hunting] Introduce threat hunting home experience Nov 5, 2025
@abhishekbhatia1710 abhishekbhatia1710 added backport:skip This PR does not require backporting release_note:feature Makes this part of the condensed release notes Team:Entity Analytics Security Entity Analytics Team labels Nov 5, 2025
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-entity-analytics (Team:Entity Analytics)

@abhishekbhatia1710 abhishekbhatia1710 requested review from hop-dev and removed request for a team November 5, 2025 10:49
@machadoum machadoum requested a review from Copilot November 5, 2025 12:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a new experimental Threat Hunting feature for Entity Analytics, providing an AI-powered interface for analyzing and investigating entity threats. The feature is controlled by the entityThreatHuntingEnabled experimental flag.

Key changes:

  • Adds a new ThreatHuntingHomePage component with AI hypotheses section, entity risk levels visualization, and entities table
  • Conditionally renders the new threat hunting page or legacy overview based on experimental flag
  • Enhances entities list table with new action buttons for timeline navigation and AI assistant
  • Updates navigation links to include threat hunting as a new entry point

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
routes.tsx Adds conditional rendering logic to show ThreatHuntingHomePage when experimental flag is enabled
routes.test.tsx Tests the conditional rendering behavior based on the experimental flag
threat_hunting_home_page.tsx New page component implementing the threat hunting UI with AI hypotheses, risk levels, and entities table
threat_hunting_home_page.test.tsx Comprehensive test coverage for the new threat hunting page component
links.ts Adds threat hunting navigation link and updates entity analytics links structure
threat_hunting_entity_risk_levels.tsx Component displaying entity risk levels with donut chart and table visualization
threat_hunting_entities_table.tsx Wrapper component for entities table in threat hunting context
use_entities_list_columns.tsx Enhances entities table with timeline and AI assistant action buttons when flag enabled
entities_list.tsx Adds rowHeight configuration for new action buttons

@abhishekbhatia1710 abhishekbhatia1710 changed the title [Security Solution][Entity Analytics][Threat Hunting] Introduce threat hunting home experience [Security Solution][Entity Analytics][Threat Hunting][WIP] Introduce threat hunting home experience Nov 5, 2025
@abhishekbhatia1710 abhishekbhatia1710 marked this pull request as draft November 5, 2025 14:14
@elasticmachine
Copy link
Contributor

elasticmachine commented Nov 5, 2025

🤖 Jobs for this PR can be triggered through checkboxes. 🚧

ℹ️ To trigger the CI, please tick the checkbox below 👇

  • Click to trigger kibana-pull-request for this PR!
  • Click to trigger kibana-deploy-project-from-pr for this PR!
  • Click to trigger kibana-deploy-cloud-from-pr for this PR!

@kibanamachine
Copy link
Contributor

Project deployments require a Github label, please add one or more of ci:project-deploy-(elasticsearch|observability|security) and trigger the job through the checkbox again.

@abhishekbhatia1710 abhishekbhatia1710 added the ci:project-deploy-security Create a Security Serverless Project label Nov 12, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 14 comments.

Comments suppressed due to low confidence (1)

x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score_kpi.tsx:117

  • The dependency array for the useEffect hook has been updated to include riskEntities (line 113), but riskEntities is recalculated on every render based on multipleEntities and singleEntity. This could cause unnecessary re-executions of the search effect.

Consider memoizing riskEntities with useMemo or deriving it directly in the dependency array from the stable rest parameters to avoid unnecessary effect triggers:

const riskEntitiesMemo = useMemo(() => {
  if (multipleEntities && multipleEntities.length > 0) {
    return multipleEntities;
  }
  return singleEntity ? [singleEntity] : [];
}, [multipleEntities, singleEntity]);

Then use riskEntitiesMemo in the effect dependencies.

  const riskEntities = useMemo(() => {
    if (multipleEntities && multipleEntities.length > 0) {
      return multipleEntities;
    }

    return singleEntity ? [singleEntity] : [];
  }, [multipleEntities, singleEntity]);

  const primaryEntity = riskEntities[0] ?? singleEntity;
  const shouldSkip = skip || riskEntities.length === 0;

  useEffect(() => {
    if (!shouldSkip && defaultIndex && !isStatusLoading && riskEngineHasBeenEnabled) {
      search({
        filterQuery,
        defaultIndex: [defaultIndex],
        entity: primaryEntity,
        entities: riskEntities,
        timerange: requestTimerange,
      });
    }
  }, [
    defaultIndex,
    search,
    filterQuery,
    shouldSkip,
    primaryEntity,
    riskEntities,
    requestTimerange,
    isStatusLoading,
    riskEngineHasBeenEnabled,
  ]);

abhishekbhatia1710 and others added 12 commits November 13, 2025 20:05
…ity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx

Co-authored-by: Copilot <[email protected]>
…et_inventory/hooks/use_asset_inventory_url_state/use_asset_inventory_url_state.ts

Co-authored-by: Copilot <[email protected]>
…et_inventory/components/asset_inventory_data_table.tsx

Co-authored-by: Copilot <[email protected]>
…ity_analytics/components/threat_hunting/entity_risk_levels/index.tsx

Co-authored-by: Copilot <[email protected]>
…rch_strategy/security_solution/factory/risk_score/kpi/query.kpi_risk_score.dsl.ts

Co-authored-by: Copilot <[email protected]>
…ity_analytics/components/threat_hunting/entities_table.tsx

Co-authored-by: Copilot <[email protected]>
…ity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx

Co-authored-by: Copilot <[email protected]>
…rch_strategy/security_solution/factory/risk_score/kpi/index.ts

Co-authored-by: Copilot <[email protected]>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.

Comment on lines 64 to 99
const usePageSizePreference = (localStorageKey?: string) => {
const [pageSize, setPageSize] = useState<number>(() => {
if (!localStorageKey || typeof window === 'undefined') {
return DEFAULT_VISIBLE_ROWS_PER_PAGE;
}

const storedValue = window.localStorage.getItem(localStorageKey);
const parsedValue = storedValue != null ? Number(JSON.parse(storedValue)) : NaN;

return Number.isFinite(parsedValue) && parsedValue > 0
? parsedValue
: DEFAULT_VISIBLE_ROWS_PER_PAGE;
});

useEffect(() => {
if (!localStorageKey || typeof window === 'undefined') {
return;
}

try {
window.localStorage.setItem(localStorageKey, JSON.stringify(pageSize));
} catch (error) {
// noop - best effort persistence
}
}, [localStorageKey, pageSize]);

const setPageSizePreference = useCallback((value: SetStateAction<number | undefined>) => {
setPageSize((previous) => {
const resolvedValue = typeof value === 'function' ? value(previous) : value;
const nextValue = resolvedValue ?? DEFAULT_VISIBLE_ROWS_PER_PAGE;
return nextValue > 0 ? nextValue : previous;
});
}, []);

return { pageSize, setPageSize: setPageSizePreference };
};
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The usePageSizePreference hook is duplicating functionality that already exists in use_page_size.ts. Both hooks:

  • Store/retrieve page size from localStorage
  • Handle validation and defaults
  • Provide the same interface

Consider removing this inline implementation and importing usePageSize from ./use_page_size instead, or if the optional localStorageKey parameter is needed, refactor use_page_size.ts to accept an optional key parameter rather than duplicating the entire hook logic.

Copilot uses AI. Check for mistakes.
Comment on lines 51 to 86
const supportedEntities = requestedEntities.filter(
(entityType) => EntityTypeToLevelField[entityType] !== RiskScoreFields.unsupported
);

const entitiesToProcess = supportedEntities.length > 0 ? supportedEntities : requestedEntities;

const accumulateBuckets = (
accumulator: Record<RiskSeverity, number>,
buckets: AggBucket[]
): Record<RiskSeverity, number> => {
const result = { ...accumulator };
buckets.forEach((bucket) => {
const key = bucket.key;
const currentTotal = result[key] ?? 0;
const bucketValue = getOr(0, 'unique_entries.value', bucket);
result[key] = currentTotal + bucketValue;
});
return result;
};

const rawAggregations = (
response.rawResponse as {
aggregations?: Record<string, AggregationBucket>;
}
).aggregations;

let aggregatedResult: Record<RiskSeverity, number> = {} as Record<RiskSeverity, number>;

if (entitiesToProcess.length <= 1) {
const riskBuckets = rawAggregations?.risk?.buckets ?? [];
aggregatedResult = accumulateBuckets(aggregatedResult, riskBuckets);
} else {
entitiesToProcess.forEach((entityType) => {
const buckets = rawAggregations?.[entityType]?.buckets ?? [];
aggregatedResult = accumulateBuckets(aggregatedResult, buckets);
});
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The parser has the same issue as the DSL builder. When all requested entities are unsupported, entitiesToProcess will include unsupported entity types (line 55), and the code will attempt to access aggregation buckets using unsupported entity type names as keys (line 84).

Since the DSL builder won't create aggregations for these entity types (or will create invalid ones), rawAggregations?.[entityType] will be undefined, resulting in empty buckets. While this doesn't crash, it's inefficient and inconsistent.

The parser should use the same logic as recommended for the DSL: if supportedEntities.length === 0, return empty results early rather than falling back to unsupported entities.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +103
riskEngineHasBeenEnabled &&
primaryEntity
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

Redundant null check at line 103. The condition primaryEntity is already guaranteed to be truthy because:

  • primaryEntity = riskEntities[0] ?? singleEntity (line 94)
  • The check shouldSkip = skip || riskEntities.length === 0 (line 95) returns early if riskEntities is empty
  • If riskEntities[0] exists (length > 0), then primaryEntity will be truthy
  • The only case where primaryEntity could be falsy is when riskEntities.length === 0, but that's already handled by shouldSkip

Remove the primaryEntity check from line 103 as it's redundant with the shouldSkip check.

Suggested change
riskEngineHasBeenEnabled &&
primaryEntity
riskEngineHasBeenEnabled

Copilot uses AI. Check for mistakes.
Comment on lines 72 to 104
const identifierField = EntityTypeToIdentifierField[entityType];
const rawIdentifier = get(identifierField, record);
const identifierCandidates = toArray(rawIdentifier);

// Use entity.name as primary display name (same as used in the name column)
// Fall back to identifier or entity.id if name is not available
const displayName =
record.entity?.name ||
identifierCandidates.find(
(value): value is string => typeof value === 'string' && value.length > 0
) ||
record.entity?.id;

const flyoutKey = EntityPanelKeyByType[entityType];

const onClick = () => {
const id = EntityPanelKeyByType[entityType];

if (id) {
openRightPanel({
id,
params: {
[EntityPanelParamByType[entityType] ?? '']: value,
contextID: ENTITIES_LIST_TABLE_ID,
scopeId: ENTITIES_LIST_TABLE_ID,
},
});
if (!flyoutKey || !displayName) {
return;
}

openRightPanel({
id: flyoutKey,
params: {
[EntityPanelParamByType[entityType] ?? '']: displayName,
contextID: ENTITIES_LIST_TABLE_ID,
scopeId: ENTITIES_LIST_TABLE_ID,
},
});
};

if (!value || !EntityPanelKeyByType[entityType]) {
const timelineIdentifier = identifierCandidates.find(
(value): value is string => typeof value === 'string' && value.length > 0
);
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

Duplicate identifier extraction logic. The code extracts identifierCandidates (lines 72-74) and then searches for a valid string identifier twice:

  1. Lines 80-82: To populate displayName
  2. Lines 102-104: To populate timelineIdentifier

Both searches perform the exact same operation. Extract this once and reuse:

const validIdentifier = identifierCandidates.find(
  (value): value is string => typeof value === 'string' && value.length > 0
);
const displayName = record.entity?.name || validIdentifier || record.entity?.id;
const timelineIdentifier = validIdentifier;

Copilot uses AI. Check for mistakes.
abhishekbhatia1710 and others added 6 commits November 14, 2025 11:13
…rch_strategy/security_solution/factory/risk_score/kpi/query.kpi_risk_score.dsl.ts

Co-authored-by: Copilot <[email protected]>
…et_inventory/components/asset_inventory_data_table.tsx

Co-authored-by: Copilot <[email protected]>
…ity_analytics/components/threat_hunting/entities_table.tsx

Co-authored-by: Copilot <[email protected]>
@abhishekbhatia1710
Copy link
Contributor Author

/ci

@elasticmachine
Copy link
Contributor

elasticmachine commented Nov 26, 2025

💔 Build Failed

Failed CI Steps

History

cc @abhishekbhatia1710

@abhishekbhatia1710
Copy link
Contributor Author

Closing this as a new PR is created instead : #243311

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

Labels

backport:skip This PR does not require backporting ci:project-deploy-security Create a Security Serverless Project release_note:feature Makes this part of the condensed release notes Team:Entity Analytics Security Entity Analytics Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants