[Workday][Sign-on] Add Sign-on data stream#20039
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Elastic Docs Style Checker (Vale)Summary: 2 suggestions found 💡 Suggestions (2): Optional style improvements. Apply when helpful.
The Vale linter checks documentation changes against the Elastic Docs style guide. To use Vale locally or report issues, refer to Elastic style guide for Vale. |
Co-authored-by: Cursor <cursoragent@cursor.com>
🚀 Benchmarks reportTo see the full report comment with |
Co-authored-by: Cursor <cursoragent@cursor.com>
|
✅ All changelog entries have the correct PR link. |
💔 Build Failed
Failed CI StepsHistory
|
TL;DR
Remediation
Investigation detailsRoot CauseThe PR moves Activity-specific required variables into the Activity data stream manifest:
That layout matched the old manifest, but after this PR the required Activity variables must be supplied through Evidence
Verification
What is this? | From workflow: PR Buildkite Detective Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not. |
|
Pinging @elastic/security-service-integrations (Team:Security-Service Integrations) |
| ) | ||
| : | ||
| { | ||
| "events": [{ |
There was a problem hiding this comment.
Severity: 🟠 High confidence: medium path: packages/workday/data_stream/sign_on/agent/stream/cel.yml.hbs:36
Sign-on CEL emits API errors wrapped inside message, so ctx.error is never set and the pipeline's error handling (terminate/pipeline_error) never fires — API errors are indexed as normal sign-on events. Emit the single-object {"events": {"error": {...}}} shape like the activity stream.
Details
On a non-200 response the program returns "events": [{"message": {"error": {...}}.encode_json()}]. Because the error object is embedded in the event's message string, the CEL input does not populate ctx.error. The ingest pipeline then renames message -> event.original and JSON-decodes it into workday.sign_on, so the error ends up at workday.sign_on.error.* (an undeclared field) and ctx.error.message stays null. As a result the pipeline's terminate (tag data_collection_error) never triggers, event.kind is never set to pipeline_error, degraded status is not reported, and every API failure is silently indexed as a normal user-signon event. The sibling activity stream uses the correct single-object form "events": {"error": {...}}, which is exactly what this pipeline's error machinery expects.
Recommendation:
Return the error in the single-object form so the framework sets ctx.error and the existing terminate/pipeline_error logic works:
:
{
"events": {
"error": {
"code": string(resp.StatusCode),
"id": resp.Status,
"message": "GET " + state.url + ": " + (
(size(resp.Body) != 0) ?
string(resp.Body)
:
resp.Status + " (" + string(resp.StatusCode) + ")"
),
},
},
}
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
There was a problem hiding this comment.
Yes, can you explain why you've done this this way?
| ignore_missing: true | ||
| description: The `message` field is no longer required if the document has an `event.original` field. | ||
| if: ctx.event?.original != null | ||
| - json: |
There was a problem hiding this comment.
Severity: 🟠 High confidence: medium path: packages/workday/data_stream/sign_on/elasticsearch/ingest_pipeline/default.yml:35
Sign-on ingests the full custom report every interval with no _id fingerprint, so each poll re-indexes the same rows as new documents (unbounded duplication). Add a fingerprint processor that derives a stable _id, as the activity pipeline does.
Details
The sign-on CEL program fetches state.url (the RaaS report) on every interval with no time-window parameters and no cursor, emitting every Report_Entry row. The pipeline never sets a deterministic _id (unlike the activity pipeline, which fingerprints requestTime/sessionId/taskId into _id). Because a custom report returns a rolling window of sign-ons, each 24h poll re-emits rows already ingested on prior runs, and Elasticsearch auto-generates a fresh _id for each, producing duplicate documents on every cycle.
Recommendation:
Fingerprint each record into _id so re-collected rows overwrite instead of duplicating. Add after the json_event_original processor:
- fingerprint:
fields:
- event.original
tag: fingerprint_sign_on
target_field: _id
ignore_missing: true🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| "events": (has(body.Report_Entry) && size(body.Report_Entry) > 0) ? | ||
| body.Report_Entry.map(e, {"message": e.encode_json()}) | ||
| : | ||
| [{"message": "{}"}], |
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: medium path: packages/workday/data_stream/sign_on/agent/stream/cel.yml.hbs:31
When the report is empty the CEL emits [{"message": "{}"}], which the pipeline turns into a spurious user-signon event (empty workday.sign_on, timestamp defaulted to ingest time) on every empty poll. Emit an empty events array instead.
Details
The empty-report branch returns [{"message": "{}"}]. The pipeline decodes this into an empty workday.sign_on map (later removed) but still unconditionally sets event.kind=event, event.category=[authentication,session], event.type=[start], and event.action=user-signon, and @timestamp falls back to ingest time. So each polling cycle with no report rows produces a junk authentication event with no user, IP, or real data. There is no cursor to advance here, so no placeholder event is needed.
Recommendation:
Return an empty events array when there are no report entries:
"events": (has(body.Report_Entry) && size(body.Report_Entry) > 0) ?
body.Report_Entry.map(e, {"message": e.encode_json()})
:
[],
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| @@ -0,0 +1 @@ | |||
| data_retention: "30d" | |||
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: medium path: packages/workday/data_stream/sign_on/lifecycle.yml:1
The sign_on data stream defines both a custom ILM policy (ilm_policy + elasticsearch/ilm/default_policy.json) and a Data Stream Lifecycle (lifecycle.yml with data_retention). Only one retention mechanism should be used; keep the ILM policy and remove lifecycle.yml.
Details
manifest.yml sets ilm_policy: logs-workday.sign_on-default_policy and ships elasticsearch/ilm/default_policy.json (hot rollover + 30d delete), while lifecycle.yml also declares a DSL data_retention: "30d". When both an ILM policy and a data stream lifecycle are attached, ILM takes precedence and the DSL retention is effectively ignored, making the two configs redundant and contradictory. The sibling activity data stream uses neither (Fleet default), so this is also inconsistent within the package.
Recommendation:
Pick one mechanism. Since the ILM policy provides rollover that DSL does not, keep the ILM policy and delete the DSL file:
# Remove packages/workday/data_stream/sign_on/lifecycle.yml.
# Retention/rollover is already managed by the ILM policy referenced in manifest.yml:
# ilm_policy: logs-workday.sign_on-default_policy🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| dependencies: | ||
| ecs: | ||
| reference: git@v9.3.0 | ||
| reference: git@v9.4.0 |
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: medium path: packages/workday/_dev/build/build.yml:3
build.yml bumps the ECS pin to v9.4.0, but the existing activity pipeline still sets ecs.version: 9.3.0, leaving the package build ECS version and the activity pipeline inconsistent. Bump the activity pipeline to 9.4.0 to match.
Details
This PR raises the ECS dependency pin from git@v9.3.0 to git@v9.4.0. The new sign_on pipeline correctly sets ecs.version: 9.4.0, but the pre-existing activity pipeline (data_stream/activity/elasticsearch/ingest_pipeline/default.yml) still hardcodes ecs.version: 9.3.0. After this change the two are inconsistent: activity documents report ECS 9.4.0 while the package is built against ECS 9.4.0 mappings. The bump should be applied consistently across all pipelines in the package.
Recommendation:
Update the activity pipeline's set_ecs_version processor to match the new build pin:
- set:
field: ecs.version
tag: set_ecs_version
value: 9.4.0🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| tag: rename_signon_ip_address_to_source_ip | ||
| target_field: workday.sign_on.signon_ip_address_string | ||
| ignore_missing: true | ||
| - set: |
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: medium path: packages/workday/data_stream/sign_on/elasticsearch/ingest_pipeline/default.yml:251
The sign_on pipeline sets source.ip but performs no geo/ASN enrichment, unlike the sibling activity pipeline. Add geoip (city + ASN) enrichment on source.ip so the Top Source IPs and location analytics work.
Details
source.ip is populated from Signon_IP_Address/Session_IP_Address, and the sign_on dashboard includes a Top Source IPs table, but the pipeline never enriches source.ip into source.geo or source.as. The activity pipeline in the same package does both geo and ASN enrichment. For a security/authentication data stream, geo+ASN enrichment on the originating IP is the current standard and is needed for investigation workflows.
Recommendation:
Add geo and ASN enrichment after source.ip is set (mirroring the activity pipeline):
- geoip:
field: source.ip
tag: geoip_source_ip_to_source_geo
target_field: source.geo
ignore_missing: true
- geoip:
database_file: GeoLite2-ASN.mmdb
field: source.ip
tag: geoip_source_ip_to_source_as
target_field: source.as
properties:
- asn
- organization_name
ignore_missing: true
- rename:
field: source.as.asn
tag: rename_source_as_asn
target_field: source.as.number
ignore_missing: true
- rename:
field: source.as.organization_name
tag: rename_source_as_organization_name
target_field: source.as.organization.name
ignore_missing: true🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| value: end | ||
| allow_duplicates: false | ||
| if: ctx.event?.end != null | ||
| - set: |
There was a problem hiding this comment.
Severity: 🟡 Medium confidence: medium path: packages/workday/data_stream/sign_on/elasticsearch/ingest_pipeline/default.yml:333
Authentication events never set event.outcome, even though the report exposes Failed_Signon and Authentication_Failure_Message. Set event.outcome to success/failure so auth analytics and detections can distinguish failed sign-ons.
Details
The pipeline categorizes events as authentication/session and copies Authentication_Failure_Message into event.reason, but never sets event.outcome. For an authentication data stream, event.outcome (success/failure) is a core ECS field driving failed-sign-on dashboards and security detections. The source data supports it directly via the Failed_Signon boolean (kept in workday.sign_on).
Recommendation:
Derive event.outcome from Failed_Signon (add before the flags are otherwise consumed):
- set:
field: event.outcome
tag: set_event_outcome_failure
value: failure
if: ctx.workday?.sign_on?.Failed_Signon == true || ctx.event?.reason != null
- set:
field: event.outcome
tag: set_event_outcome_success
value: success
if: ctx.event?.outcome == null🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| type: keyword | ||
| description: The Workday account associated with the sign-on (alternate report column name for System_Account). | ||
| - name: signin_ip_address_string | ||
| type: keyword |
There was a problem hiding this comment.
Severity: 🔵 Low confidence: medium path: packages/workday/data_stream/sign_on/fields/fields.yml:89
fields.yml declares signon_ip_address_string typo variant signin_ip_address_string that the pipeline never produces. Remove the unused field definition.
Details
fields.yml defines workday.sign_on.signin_ip_address_string ("sign-in"), but the pipeline only ever produces signon_ip_address_string (the Signon_IP_Address fallback rename) and session_ip_address_string. No processor or report column maps to signin_ip_address_string, so it is dead and appears to be a typo of the already-declared signon_ip_address_string.
Recommendation:
Remove the unused field definition (keep signon_ip_address_string, which the pipeline actually sets):
# Delete this entry — the pipeline emits `signon_ip_address_string`, not `signin_ip_address_string`:
# - name: signin_ip_address_string
# type: keyword
# description: The originating address of the sign-on when it is not a routable IP address (for example, Workday Internal).🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
| "id": "", | ||
| "params": { | ||
| "fontSize": 12, | ||
| "markdown": "### Overview\n\nThe Workday Sign-On dashboard provides security and IT teams visibility into authentication activity from the Workday Sign-On custom report, covering sign-on volume, authentication methods, and failure patterns.\n\nThe Total Users tile shows authenticated account scope, while Sign-ons by Authentication Type and Distribution of Authentication Types break down usage across SAML, OAuth 2.0, Trusted, Username/Password, and ID Token.\n\nTop Source IPs surfaces active originating addresses for investigation, Top SAML Identity Providers highlights federated sign-in sources, and the Failure Breakdown table helps teams quickly triage authentication failures.\n\n[**Integration Page**](https://127.0.0.1:5601/app/integrations/detail/workday-0.1.0/overview)\n", |
There was a problem hiding this comment.
Severity: 🔵 Low confidence: low path: packages/workday/kibana/dashboard/workday-36544d7d-809c-4306-a2b0-6276bad0a9b6.json:272
The sign_on dashboard's overview markdown links to a version-pinned integration page (workday-0.1.0), which is already stale (package is 0.1.1) and will drift on every release. Link to the unversioned integration page.
Details
The Overview markdown panel embeds https://127.0.0.1:5601/app/integrations/detail/workday-0.1.0/overview. The version segment 0.1.0 is already out of date for this PR (which ships 0.1.1) and will become stale again on each subsequent release, sending users to a non-current integration detail page.
Recommendation:
Drop the version from the integration link so it always resolves to the current package:
[**Integration Page**](https://127.0.0.1:5601/app/integrations/detail/workday/overview)
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
⚠️ Automated review — verify suggestions before applying.
Review summaryIssues found across the latest commits bd625d5 — 2 high, 5 medium, 2 low
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
|
| "message": { | ||
| "error": { | ||
| "code": string(resp.StatusCode), | ||
| "status": string(resp.Status), |
There was a problem hiding this comment.
| "status": string(resp.Status), | |
| "status": resp.Status, |
| "body": (size(resp.Body) != 0) ? | ||
| string(resp.Body) | ||
| : | ||
| string(resp.Status) + " (" + string(resp.StatusCode) + ")", |
There was a problem hiding this comment.
| string(resp.Status) + " (" + string(resp.StatusCode) + ")", | |
| resp.Status + " (" + string(resp.StatusCode) + ")", |
| ) | ||
| : | ||
| { | ||
| "events": [{ |
There was a problem hiding this comment.
Yes, can you explain why you've done this this way?
| "events": (has(body.Report_Entry) && size(body.Report_Entry) > 0) ? | ||
| body.Report_Entry.map(e, {"message": e.encode_json()}) | ||
| : | ||
| [{"message": "{}"}], |
There was a problem hiding this comment.
This program is non-idiomatic as raised by Vera. If this is necessary, it needs a comment explaining why.
| title: Collect Workday logs via API | ||
| description: Collecting logs from Workday via CEL input. | ||
| vars: | ||
| - name: hostname |
There was a problem hiding this comment.
This is using visualisations by reference. This is against guidance. Can you make sure the visualisations are saved by value.
Proposed commit message
Checklist
changelog.ymlfile.How to test this PR locally
To test the Workday package:
Related issues
Screenshots