Skip to content

Conversation

@efd6
Copy link
Contributor

@efd6 efd6 commented Sep 24, 2025

Proposed commit message

o365: fix error propagation within cel program

The logic for handling error propagation from failed subscriptions
requests was incorrect. The downstream logic was expecting to find the
error in the root of state, but the subscription request block was
leaving it in the work object.

Later error handling was incorrectly leaving any error state in the root
of state, when it should have been placed under events.

In the case that a subscription has failed, there may not be an entry in
the last_for list for the subscription, resulting in a look-up failure
during assessment for completion of the hour-window steps.

Finally, the postamble logic was brittle to failed requests, assuming
that the todo_type and todo_content list would be present, which is not
necessarily the case when a subscription has failed.

This has been manually tested against the mock committed in the deploy
directory (with removal of authentication for convenience) with the
following outcomes:

only one, invalid, subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.TypeRequiringAdditionalPermissions"
	}
}
-- out.json --
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": false,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": []
	}
}
"""

start with an invalid subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint"
	}
}
-- out.json --
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": true,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": [
			"Audit.SharePoint"
		]
	}
}
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"cursor": {
		"last_for": {
			"audit.sharepoint": "2025-09-23T13:07:36.915764483Z"
		}
	},
	"events": [
		{
… more …
"""

end with an invalid subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.SharePoint, Audit.TypeRequiringAdditionalPermissions"
	}
}
-- out.json --
… more before …
			"enabled": true,
			"type": "Audit.SharePoint"
		},
		"todo_content": [],
		"todo_type": [
			"Audit.TypeRequiringAdditionalPermissions"
		]
	}
}
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.SharePoint, Audit.TypeRequiringAdditionalPermissions",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"cursor": {
		"last_for": {
			"audit.sharepoint": "2025-09-24T00:10:30.808672557Z"
		}
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": false,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": []
	}
}
"""

Checklist

  • I have reviewed tips for building integrations and this pull request is aligned with them.
  • I have verified that all data streams collect metrics or logs.
  • I have added an entry to my package's changelog.yml file.
  • I have verified that Kibana version constraints are current according to guidelines.
  • I have verified that any added dashboard complies with Kibana's Dashboard good practices

Author's Checklist

  • [ ]

How to test this PR locally

Related issues

Screenshots

@efd6 efd6 self-assigned this Sep 24, 2025
@efd6 efd6 added Integration:o365 Microsoft Office 365 bugfix Pull request that fixes a bug issue Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations] labels Sep 24, 2025
The logic for handling error propagation from failed subscriptions
requests was incorrect. The downstream logic was expecting to find the
error in the root of state, but the subscription request block was
leaving it in the work object.

Later error handling was incorrectly leaving any error state in the root
of state, when it should have been placed under events.

In the case that a subscription has failed, there may not be an entry in
the last_for list for the subscription, resulting in a look-up failure
during assessment for completion of the hour-window steps.

Finally, the postamble logic was brittle to failed requests, assuming
that the todo_type and todo_content list would be present, which is not
necessarily the case when a subscription has failed.

This has been manually tested against the mock committed in the deploy
directory (with removal of authentication for convenience) with the
following outcomes:

only one, invalid, subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.TypeRequiringAdditionalPermissions"
	}
}
-- out.json --
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": false,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": []
	}
}
"""

start with an invalid subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint"
	}
}
-- out.json --
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": true,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": [
			"Audit.SharePoint"
		]
	}
}
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"cursor": {
		"last_for": {
			"audit.sharepoint": "2025-09-23T13:07:36.915764483Z"
		}
	},
	"events": [
		{
… more …
"""

end with an invalid subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.SharePoint, Audit.TypeRequiringAdditionalPermissions"
	}
}
-- out.json --
… more before …
			"enabled": true,
			"type": "Audit.SharePoint"
		},
		"todo_content": [],
		"todo_type": [
			"Audit.TypeRequiringAdditionalPermissions"
		]
	}
}
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.SharePoint, Audit.TypeRequiringAdditionalPermissions",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"cursor": {
		"last_for": {
			"audit.sharepoint": "2025-09-24T00:10:30.808672557Z"
		}
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": false,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": []
	}
}
"""
@efd6 efd6 force-pushed the s6431-o365-error_propagation branch from 8d726de to 0e26446 Compare September 24, 2025 00:27
@elastic-vault-github-plugin-prod

🚀 Benchmarks report

To see the full report comment with /test benchmark fullreport

@elasticmachine
Copy link

💚 Build Succeeded

cc @efd6

@elastic-sonarqube
Copy link

@efd6 efd6 marked this pull request as ready for review September 24, 2025 01:31
@efd6 efd6 requested a review from a team as a code owner September 24, 2025 01:31
@elasticmachine
Copy link

Pinging @elastic/security-service-integrations (Team:Security-Service Integrations)

Copy link
Contributor

@kcreddy kcreddy left a comment

Choose a reason for hiding this comment

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

LGTM. Minor clarification

Comment on lines +469 to +473
state.work.curr_type.to_lower().as(curr_type, curr_type in last_for ?
timestamp(last_for[curr_type]) < now - duration("1h")
:
false
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
state.work.curr_type.to_lower().as(curr_type, curr_type in last_for ?
timestamp(last_for[curr_type]) < now - duration("1h")
:
false
)
state.work.curr_type.to_lower().as(curr_type, curr_type in last_for && timestamp(last_for[curr_type]) < now - duration("1h"))

Can we do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer the clarity that is in the current version (not really significant here, but the current version is also more efficient since of !curr_type in last_for then the timestamp parsing is not done, while it is in the proposed version).

@efd6 efd6 merged commit 6c447ac into elastic:main Sep 24, 2025
9 checks passed
@elastic-vault-github-plugin-prod

Package o365 - 2.29.2 containing this change is available at https://epr.elastic.co/package/o365/2.29.2/

tehbooom pushed a commit to tehbooom/integrations that referenced this pull request Nov 19, 2025
The logic for handling error propagation from failed subscriptions
requests was incorrect. The downstream logic was expecting to find the
error in the root of state, but the subscription request block was
leaving it in the work object.

Later error handling was incorrectly leaving any error state in the root
of state, when it should have been placed under events.

In the case that a subscription has failed, there may not be an entry in
the last_for list for the subscription, resulting in a look-up failure
during assessment for completion of the hour-window steps.

Finally, the postamble logic was brittle to failed requests, assuming
that the todo_type and todo_content list would be present, which is not
necessarily the case when a subscription has failed.

This has been manually tested against the mock committed in the deploy
directory (with removal of authentication for convenience) with the
following outcomes:

only one, invalid, subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.TypeRequiringAdditionalPermissions"
	}
}
-- out.json --
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": false,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": []
	}
}
"""

start with an invalid subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint"
	}
}
-- out.json --
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": true,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": [
			"Audit.SharePoint"
		]
	}
}
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.TypeRequiringAdditionalPermissions, Audit.SharePoint",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"cursor": {
		"last_for": {
			"audit.sharepoint": "2025-09-23T13:07:36.915764483Z"
		}
	},
	"events": [
		{
… more …
"""

end with an invalid subscription:
"""
-- data.json --
{
	"url": "http://localhost:9001",
	"want_more": false,
	"base": {
		"tenant_id": "test-cel-tenant-id",
		"list_contents_start_time": "12h",
		"batch_interval": "1h",
		"maximum_age": "167h55m",
		"content_types": "Audit.SharePoint, Audit.TypeRequiringAdditionalPermissions"
	}
}
-- out.json --
… more before …
			"enabled": true,
			"type": "Audit.SharePoint"
		},
		"todo_content": [],
		"todo_type": [
			"Audit.TypeRequiringAdditionalPermissions"
		]
	}
}
{
	"base": {
		"batch_interval": "1h",
		"content_types": "Audit.SharePoint, Audit.TypeRequiringAdditionalPermissions",
		"list_contents_start_time": "12h",
		"maximum_age": "167h55m",
		"tenant_id": "test-cel-tenant-id"
	},
	"cursor": {
		"last_for": {
			"audit.sharepoint": "2025-09-24T00:10:30.808672557Z"
		}
	},
	"events": {
		"error": {
			"code": "401",
			"id": "401 Unauthorized",
			"message": "POST /activity/feed/subscriptions/start?contentType=Audit.TypeRequiringAdditionalPermissions: {\"error\":{\"code\":\"AF10001\",\"message\":\"The permission set (...) sent in the request does not include the expected permission.\"}}"
		}
	},
	"url": "http://localhost:9001",
	"want_more": false,
	"work": {
		"curr_type": "Audit.TypeRequiringAdditionalPermissions",
		"todo_type": []
	}
}
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes a bug issue Integration:o365 Microsoft Office 365 Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants