Repository navigation
Initial release of Spycloud - #10608
Conversation
|
💚 CLA has been signed |
|
Pinging @elastic/security-service-integrations (Team:Security-Service Integrations) |
| request("GET", state.url + "/breach/catalog?since=" + ( | ||
| !state.?want_more.orValue(false) ? | ||
| ( | ||
| has(state.?cursor.last_published_date) && state.cursor.last_published_date != null ? | ||
| string((timestamp(state.cursor.last_published_date) - duration("24h"))).split("T")[0] | ||
| : | ||
| "1970-01-01" | ||
| ) | ||
| : | ||
| ( | ||
| has(state.?cursor.first_published_date) && state.cursor.first_published_date != null ? | ||
| string((timestamp(state.cursor.first_published_date) - duration("24h"))).split("T")[0] | ||
| : | ||
| "" | ||
| ) + "&cursor=" + state.pagination_token | ||
| )).with({ | ||
| "Header":{ | ||
| "x-api-key": [state.api_key], | ||
| "User-Agent": ["ElasticSearch/0.1.0"], | ||
| } | ||
| }).do_request().as(resp, | ||
| resp.StatusCode == 200 ? | ||
| bytes(resp.Body).decode_json().as(body, { | ||
| "events": (has(body.results) && body.results.size() > 0 ? | ||
| ( | ||
| body.results.map(e,{ | ||
| "message": e.encode_json() | ||
| }) | ||
| ) | ||
| : | ||
| ( | ||
| [{"message": body.encode_json()}] | ||
| ) | ||
| ), | ||
| "cursor": { | ||
| "last_published_date": ( | ||
| has(body.results) && body.results.size() > 0 ? | ||
| ( | ||
| has(state.?cursor.last_published_date) && body.results.map(e, e.spycloud_publish_date).max() < state.cursor.last_published_date ? | ||
| state.cursor.last_published_date | ||
| : | ||
| body.results.map(e, e.spycloud_publish_date).max() | ||
| ) | ||
| : | ||
| state.?cursor.last_published_date.orValue("1970-01-02T00:00:00Z") | ||
| ), | ||
| "first_published_date": ( | ||
| has(state.?cursor.first_published_date) && has(body.results) && body.results.size() > 0 ? | ||
| ( | ||
| has(body.cursor) && body.cursor != "" && state.want_more ? state.cursor.first_published_date : state.cursor.last_published_date | ||
| ) | ||
| : | ||
| "1970-01-02T00:00:00Z" | ||
| ), | ||
| }, | ||
| "pagination_token": has(body.cursor) && body.cursor != "" ? body.cursor : "", | ||
| "want_more": has(body.cursor) && body.cursor != "", | ||
| "api_key": state.api_key | ||
| }) | ||
| : | ||
| { | ||
| "events": { | ||
| "error": { | ||
| "code": string(resp.StatusCode), | ||
| "id": string(resp.Status), | ||
| "message": string(resp.Body) | ||
| } | ||
| }, | ||
| "want_more": false, | ||
| "api_key": state.api_key | ||
| } | ||
| ) |
There was a problem hiding this comment.
Untested
request(
"GET",
state.url + "/breach/catalog?" + {
?"since": (state.?want_more.orValue(false) ? state.?cursor.first_published_date : state.?cursor.last_published_date).as(date,
date.hasValue() ?
optional.of([(timestamp(date.Value()) - duration("24h")).format(time_layout.DateTime)])
:
optional.none()
),
?"cursor": state.?want_more.orValue(false) ? [state.pagination_token] : optional.none(),
}.format_query()
).with({
"Header": {
"x-api-key": [state.api_key],
"User-Agent": ["ElasticSearch/0.1.0"],
},
}).do_request().as(resp, resp.StatusCode == 200 ?
bytes(resp.Body).decode_json().as(body,
{
"events": (has(body.results) && body.results.size() > 0) ?
body.results.map(e, {"message": e.encode_json()})
:
[{"message": body.encode_json()}],
"cursor": {
?"last_published_date": (has(body.results) && body.results.size() > 0) ?
optional.of(([?state.?cursor.last_published_date] + body.results.map(e, e.spycloud_publish_date)).max())
:
state.?cursor.last_published_date,
?"first_published_date": (has(state.?cursor.first_published_date) && has(body.results) && body.results.size() > 0) ?
(
(has(body.cursor) && body.cursor != "" && state.want_more) ?
state.cursor.first_published_date
:
state.cursor.last_published_date
)
:
optional.none(),
},
"pagination_token": body.?cursor.orValue(""),
"want_more": body.?cursor.orValue("") != "",
"api_key": state.api_key,
}
)
:
{
"events": {
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),
"message": string(resp.Body),
},
},
"want_more": false,
"api_key": state.api_key,
}
)
There was a problem hiding this comment.
Did you try the code above and find that it does not work? If so, what was the error?
| fields: | ||
| - api_key | ||
| program: | | ||
| request("GET", state.url + "/compass/data?since=" + ( |
1. Implemented Readme changes. 2. Applied minify_json in config.yml. 3. Implemented all the suggested data-collection changes.
|
/test |
🚀 Benchmarks reportTo see the full report comment with |
| )).with({ | ||
| "Header":{ | ||
| "x-api-key": [state.api_key], | ||
| "User-Agent": ["ElasticSearch/0.1.0"], |
There was a problem hiding this comment.
I still don't think this is a good idea; it gives the wrong indication to the server who we are (we are not an Elasticsearch instance). Also the "S" should be lowercase.
| fields: | ||
| - api_key | ||
| program: | | ||
| state.?since_data.orValue(false) ? |
There was a problem hiding this comment.
https://pkg.go.dev/github.com/elastic/mito/lib#hdr-Global_Variables and https://pkg.go.dev/time#pkg-constants. But the constants are just strings and work the same way that Go time formats work, so if one does not exist, you can just use the literal, "2006-01-02".
| "first_modified_date": state.?cursor.first_modified_date.orValue((now - duration(state.initial_interval)).format(time_layout.RFC3339)), | ||
| "last_modified_date": state.?cursor.last_modified_date.orValue((now - duration(state.initial_interval)).format(time_layout.RFC3339)), | ||
| }, | ||
| "pagination_token_since": first_body.?cursor.orValue(""), |
There was a problem hiding this comment.
| "pagination_token_since": first_body.?cursor.orValue(""), | |
| ?"pagination_token_since": first_body.?cursor, |
?
| "since_modified_data": true, | ||
| } | ||
| ) | ||
| : |
| redact: | ||
| fields: | ||
| - api_key | ||
| program: | |
| has(state.?cursor.last_published_date) && state.cursor.last_published_date != null ? | ||
| string((timestamp(state.cursor.last_published_date) - duration("24h"))).split("T")[0] | ||
| : | ||
| "1970-01-01" |
There was a problem hiding this comment.
Is this documented as being a required parameter? i.e. if the parameter were not present would we get the same behaviour?
| request("GET", state.url + "/breach/catalog?since=" + ( | ||
| !state.?want_more.orValue(false) ? | ||
| ( | ||
| has(state.?cursor.last_published_date) && state.cursor.last_published_date != null ? | ||
| string((timestamp(state.cursor.last_published_date) - duration("24h"))).split("T")[0] | ||
| : | ||
| "1970-01-01" | ||
| ) | ||
| : | ||
| ( | ||
| has(state.?cursor.first_published_date) && state.cursor.first_published_date != null ? | ||
| string((timestamp(state.cursor.first_published_date) - duration("24h"))).split("T")[0] | ||
| : | ||
| "" | ||
| ) + "&cursor=" + state.pagination_token | ||
| )).with({ | ||
| "Header":{ | ||
| "x-api-key": [state.api_key], | ||
| "User-Agent": ["ElasticSearch/0.1.0"], | ||
| } | ||
| }).do_request().as(resp, | ||
| resp.StatusCode == 200 ? | ||
| bytes(resp.Body).decode_json().as(body, { | ||
| "events": (has(body.results) && body.results.size() > 0 ? | ||
| ( | ||
| body.results.map(e,{ | ||
| "message": e.encode_json() | ||
| }) | ||
| ) | ||
| : | ||
| ( | ||
| [{"message": body.encode_json()}] | ||
| ) | ||
| ), | ||
| "cursor": { | ||
| "last_published_date": ( | ||
| has(body.results) && body.results.size() > 0 ? | ||
| ( | ||
| has(state.?cursor.last_published_date) && body.results.map(e, e.spycloud_publish_date).max() < state.cursor.last_published_date ? | ||
| state.cursor.last_published_date | ||
| : | ||
| body.results.map(e, e.spycloud_publish_date).max() | ||
| ) | ||
| : | ||
| state.?cursor.last_published_date.orValue("1970-01-02T00:00:00Z") | ||
| ), | ||
| "first_published_date": ( | ||
| has(state.?cursor.first_published_date) && has(body.results) && body.results.size() > 0 ? | ||
| ( | ||
| has(body.cursor) && body.cursor != "" && state.want_more ? state.cursor.first_published_date : state.cursor.last_published_date | ||
| ) | ||
| : | ||
| "1970-01-02T00:00:00Z" | ||
| ), | ||
| }, | ||
| "pagination_token": has(body.cursor) && body.cursor != "" ? body.cursor : "", | ||
| "want_more": has(body.cursor) && body.cursor != "", | ||
| "api_key": state.api_key | ||
| }) | ||
| : | ||
| { | ||
| "events": { | ||
| "error": { | ||
| "code": string(resp.StatusCode), | ||
| "id": string(resp.Status), | ||
| "message": string(resp.Body) | ||
| } | ||
| }, | ||
| "want_more": false, | ||
| "api_key": state.api_key | ||
| } | ||
| ) |
There was a problem hiding this comment.
Did you try the code above and find that it does not work? If so, what was the error?
1. Replace the time format with the string literal. 2. Added dynamic mappings for the url.*. 3. Removed the first_published cursor as not required. 4. Implemented the suggested data-collection changes
| state.url + "/breach/catalog?" + { | ||
| ?"since": (state.?want_more.orValue(false) ? optional.none() | ||
| : | ||
| has(state.?cursor.last_published_date) ? | ||
| optional.of([(timestamp(state.cursor.last_published_date) - duration("24h")).format("2006-01-02")]) | ||
| : | ||
| optional.none() | ||
| ), |
There was a problem hiding this comment.
| state.url + "/breach/catalog?" + { | |
| ?"since": (state.?want_more.orValue(false) ? optional.none() | |
| : | |
| has(state.?cursor.last_published_date) ? | |
| optional.of([(timestamp(state.cursor.last_published_date) - duration("24h")).format("2006-01-02")]) | |
| : | |
| optional.none() | |
| ), | |
| state.url + "/breach/catalog?" + { | |
| ?"since": (state.?want_more.orValue(false) || !has(state.?cursor.last_published_date)) ? | |
| optional.none() | |
| : | |
| optional.of([(timestamp(state.cursor.last_published_date) - duration("24h")).format("2006-01-02")]), |
| [{"message": body.encode_json()}], | ||
| "cursor": { | ||
| ?"last_published_date": (has(body.results) && body.results.size() > 0) ? | ||
| optional.of(([?state.?cursor.last_published_date] + body.results.map(e, e.spycloud_publish_date)).max()) |
There was a problem hiding this comment.
What is the format of spycloud_publish_date? I assume this is a date format. "2006-01-02"? While this is not strictly a timestamp (it's a string), it does always sort the same way as the corresponding timestamp, so it should be ok to leave as a string. Is this a correct assessment?
| ( | ||
| first_body.results.map(e, {"message": e.encode_json()}) | ||
| ) |
There was a problem hiding this comment.
| ( | |
| first_body.results.map(e, {"message": e.encode_json()}) | |
| ) | |
| first_body.results.map(e, {"message": e.encode_json()}) |
| ?"last_published_date": (has(first_body.results) && first_body.results.size() > 0) ? | ||
| optional.of(([?state.?cursor.last_published_date] + first_body.results.map(e, e.spycloud_publish_date)).max()) |
There was a problem hiding this comment.
What format are the timestamps here? If they are not lexically sortable with the same ordering as the chronological sort this is not correct. See #10608 (comment).
| [{"message": body.encode_json()}], | ||
| "cursor": { | ||
| ?"last_published_date": (has(body.results) && body.results.size() > 0) ? | ||
| optional.of(([?state.?cursor.last_published_date] + body.results.map(e, e.spycloud_publish_date)).max()) |
kcreddy
left a comment
There was a problem hiding this comment.
There are many PII and PCI information in these datasets. like passport, dob, age, address, postal_code, bank_number, credit card, ssn, etc.
@jamiehynds, do we have any policies to mask or delete such information before ingesting them?
| "device": { | ||
| "model": 890123, | ||
| "name": 456721 |
There was a problem hiding this comment.
As per developer (confirmed with Spycloud team), the values for the breach_catalog datastream are numeric, representing the actual count. Hence ECS mapping is not possible. Same for comments #10608 (comment) and #10608 (comment)
cc: @muskan-crest
1. Syntactical change in cel code. 2. Added ecs mapping in breach record and compass. 3. Masked the user sensitive details.
|
/test |
| ?"since": (state.?want_more.orValue(false) || !has(state.?cursor.last_published_date)) ? | ||
| optional.none() | ||
| : | ||
| optional.of([(timestamp(state.cursor.last_published_date) - duration("24h")).format("2006-01-02")]), |
There was a problem hiding this comment.
Is this 24h look-back offset to prevent loss of data due to the low temporal resolution of the query parameter?
There was a problem hiding this comment.
Data is generated daily at 12:00 AM in SpyCloud. For example, on July 31st, data will be created at 12:00 AM. If the API is accessed on August 1st at 3:00 PM, the operation labeled "now - 24h" will retrieve data starting from 3:00 PM on July 31st. Consequently, data from 12:00 AM to 2:59 PM on July 31st will be excluded and not retrieved. To prevent data loss, we implement a 24-hour look-back offset.
There was a problem hiding this comment.
If the API is accessed on August 1st at 3:00 PM, the operation labeled "now - 24h" will retrieve data starting from 3:00 PM on July 31st.
Why? The "since" value does not get anything about the time of day; the format is "2006-01-02" which means that the query would be 2024-08-01. If you offset backwards by 24 hours, you will get 2024-07-31. If the query indicates to the API that it should be from the start of the day, then this offset is not needed, if it means from the end of the day, then it is.
| optional.of(string(([?state.?cursor.last_modified_date] + second_body.results.map(e, e.record_modification_date)).map(t, timestamp(t)).max())) | ||
| : | ||
| state.?cursor.last_modified_date, | ||
| "last_published_date": state.?cursor.last_published_date.orValue(null), |
There was a problem hiding this comment.
Why are we using a fallback of null here? If it is to indicate that it should not be used, let's use an optional field.
| "last_published_date": state.?cursor.last_published_date.orValue(null), | |
| ?"last_published_date": state.?cursor.last_published_date, |
Why does this differ from the case in the other main conditional branch where the fallback is to now - duration(state.initial_interval)? Note that putting that look-back deep in the program makes me queazy; I have been working on another integration that was doing this and I think that it was the cause of needless duplicate queries. Why is the look-back there?
| (state.?cursor.last_published_date).as(date, | ||
| optional.of([(date.hasValue() ? | ||
| timestamp(state.cursor.last_published_date) - duration("24h") | ||
| : | ||
| now - (duration(state.initial_interval) + duration("24h")) | ||
| ).format("2006-01-02")]) | ||
| )), |
There was a problem hiding this comment.
Please fix the indentation here.
| field: spycloud.compass.cc.bin | ||
| tag: mask_cc_bin | ||
| value: '******' | ||
| if: ctx.tags != null && ctx.tags.contains('hide_sensitive_true') && ctx.spycloud?.compass?.cc?.bin != null |
There was a problem hiding this comment.
| if: ctx.tags != null && ctx.tags.contains('hide_sensitive_true') && ctx.spycloud?.compass?.cc?.bin != null | |
| if: ctx.tags != null && ctx.tags.contains('hide_sensitive') && ctx.spycloud?.compass?.cc?.bin != null |
?
/cc @kcreddy
There was a problem hiding this comment.
Agree with hide_sensitive as the parameter name instead of hide_sensitive_true.
| - set: | ||
| field: spycloud.compass.cc.bin | ||
| tag: mask_cc_bin | ||
| value: '******' |
There was a problem hiding this comment.
Do we want to use a more searchable value?
1. Changed the mask value to REDACT. 2. Rename hide_sensitive_true to hide_sensitive. 3. Changed the cursor in breach_record, and indented the code.
|
/test |
kcreddy
left a comment
There was a problem hiding this comment.
Following are fields I can identify (only based on sample logs) that could be masked, if we want to proceed with masking sensitive PII and PCI data:
breach_catalog: None (See #10608 (comment))
breach_record:
password.plaintextpassword.value
compass:
bank_numbercc.bincc.expirationcc.last_fourcc.numberdrivers.license.numberdrivers.license.state_codenational_idpassport_numberpassword.plaintextpassword.valuepostal_codesocial_security_numberssn_last_four
@muskan-crest please check if all fields from above are accounted for into hide_sensitive option.
@andrewkroh, we are adding a user-configurable toggle hide_sensitive (default: true) to allow users to mask above sensitive content (with value REDACTED).
Do you have any concerns with this approach?
One concern is that the redaction happens in Elasticsearch instead of Elastic Agent. Could we do the redaction inside of the Elastic Agent? It looks like we have a finite set of fields and the data is JSON so this seems feasible. The other concern is usability. I don't know the exact use case for this data, but with similar datasets a question you may want to ask is was my password/CC#/etc value exposed? If you just do a string replacement you can no longer answer this question with certainty. But if do an HMAC on the value you can still answer this question if you are privileged enough to know the HMAC secret. This is how the audit logs from Hashicorp Vault operate. When a secret is returned in a response, then it writes
in the event. When the original secret value and the HMAC secret are known then this can be used to search the logs to determine if a value was accessed (or exposed in this case). So if you can pass a secret to the Agent and then run an HMAC on the list of sensitive field values we would have a better design IMO. |
| ?"since": (state.?want_more.orValue(false) || !has(state.?cursor.last_published_date)) ? | ||
| optional.none() | ||
| : | ||
| optional.of([(timestamp(state.cursor.last_published_date) - duration("24h")).format("2006-01-02")]), |
There was a problem hiding this comment.
If the API is accessed on August 1st at 3:00 PM, the operation labeled "now - 24h" will retrieve data starting from 3:00 PM on July 31st.
Why? The "since" value does not get anything about the time of day; the format is "2006-01-02" which means that the query would be 2024-08-01. If you offset backwards by 24 hours, you will get 2024-07-31. If the query indicates to the API that it should be from the start of the day, then this offset is not needed, if it means from the end of the day, then it is.
| : | ||
| (state.?cursor.last_published_date).as(date, | ||
| optional.of([(date.hasValue() ? | ||
| timestamp(state.cursor.last_published_date) - duration("24h") |
There was a problem hiding this comment.
Same question here. I am not convinced that this is correct.
There was a problem hiding this comment.
This operation is a requirement from the customer rather than from us. Since it has already been confirmed and tested by the customer, we cannot make any changes.
| optional.of([(date.hasValue() ? | ||
| timestamp(state.cursor.last_published_date) - duration("24h") | ||
| : | ||
| now - (duration(state.initial_interval) + duration("24h")) |
There was a problem hiding this comment.
This one is less of a concern, but I'm still dubious about it.
| ( | ||
| first_body.results.size() > 0 ? | ||
| first_body.results.map(e, {"message": e.encode_json()}) | ||
| : | ||
| [{"message":"No hits for Breach Catalog using since filter"}] | ||
| ) |
There was a problem hiding this comment.
Please fix the indentation of this block.
| { | ||
| "events": { | ||
| "error": { | ||
| "code": string(resp.StatusCode), | ||
| "id": string(resp.Status), | ||
| "message": string(resp.Body), | ||
| } | ||
| }, | ||
| "api_key": state.api_key, | ||
| "want_more": false, | ||
| "initial_interval": state.initial_interval, | ||
| "severity": state.severity, | ||
| "since_data": true, | ||
| "since_modified_data": true, | ||
| } |
There was a problem hiding this comment.
Please fix the indentation of this block.
Also, the resp.Body may be empty, so we synthesise a message in that case. Example here. This applies to the other cases here as well.
1. Remove the -24h logic in data collection. 2. Add hide sensitive mask to few more fields.
|
/test |
💚 Build Succeeded
History
|
|
|
Package spycloud - 0.1.0 containing this change is available at https://epr.elastic.co/search?package=spycloud |
Initial release of Spycloud Added three data streams - breach_catalog, breach_record and compass. Added data collection logic for all the three data streams. Added the ingest pipeline for all the three data streams. Mapped fields according to the ECS schema and added Fields metadata in the appropriate yml files. Added dashboards and visualizations. Added test for pipeline for all the three data streams. Added system test cases for all the three data streams.
Initial release of Spycloud Added three data streams - breach_catalog, breach_record and compass. Added data collection logic for all the three data streams. Added the ingest pipeline for all the three data streams. Mapped fields according to the ECS schema and added Fields metadata in the appropriate yml files. Added dashboards and visualizations. Added test for pipeline for all the three data streams. Added system test cases for all the three data streams.
Initial release of Spycloud Added three data streams - breach_catalog, breach_record and compass. Added data collection logic for all the three data streams. Added the ingest pipeline for all the three data streams. Mapped fields according to the ECS schema and added Fields metadata in the appropriate yml files. Added dashboards and visualizations. Added test for pipeline for all the three data streams. Added system test cases for all the three data streams.




Proposed commit message
Checklist
changelog.ymlfile.How to test this PR locally
Test-verbose.txt
Screenshots