Skip to content

CrowdStrike API call and retry mechanism#5654

Merged
san81 merged 5 commits into
opensearch-project:mainfrom
nsgupta1:main
Apr 28, 2025
Merged

CrowdStrike API call and retry mechanism#5654
san81 merged 5 commits into
opensearch-project:mainfrom
nsgupta1:main

Conversation

@nsgupta1
Copy link
Copy Markdown
Contributor

@nsgupta1 nsgupta1 commented Apr 24, 2025

Description

  • Interactions with CrowdStrike Falcon Threat Intel API
  • Token Refresh mechanism
  • Validation for paginationLink sent by CrowdStrike Falcon API

Issues Resolved

Resolves #[Issue number to be closed when this PR is merged]

Check List

  • [Y] New functionality includes testing.
  • [N] New functionality has a documentation issue. Please link to it in this PR.
    • [Y] New functionality has javadoc added
  • [Y] Commits are signed with a real name per the DCO

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Signed-off-by: nsgupta1 <nsgupta1@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@san81 san81 left a comment

Choose a reason for hiding this comment

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

Left a few comments but overall nice progress

* @param paginationLink An optional pagination URL suffix (used when fetching next pages).
* @return A {@link CrowdStrikeApiResponse} containing response body and headers.
*/
public CrowdStrikeApiResponse getAllContent(Long startTime, Long endTime, String paginationLink) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please use Instant type for startTime and endTime

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: See if using Optional<> for paginationLink adds any redability.

response.setHeaders(responseEntity.getHeaders());
return response;
} catch (Exception e) {
log.error("Error fetching CrowdStrike content from URI: {}", uri, e);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use Noisy flag to avoid repeated logging in the case of repeated failures.

log.error(NOISY, "Error fetching CrowdStrike content from URI: {}", uri, e);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also, it looks like you are not doing anything here by catching. Do you really want to catch it here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wanted to log the exact URI that's throwing error but I'm also doing it in invokeGetAPI method, I'll remove this try catch block.


// Convenience method to get a specific header
public List<String> getHeader(String headerName) {
return headers.getOrDefault(headerName, Collections.emptyList());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

headers itself could be null. Either handle null case or have only one constructor that takes both the arguments and that is the only way to initialize this instance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

CrowdStrike API returns a header even if API throws an error. I'll create a constructor that takes header and body as inputs.

//There is still time to renew, or someone else must have already renewed it
return;
}
synchronized (tokenRenewLock) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

May be it is good to have this synchronized block inside getAuthToken() method? Otherwise, there is no protection if someone call getAuthToken() concurrently

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@san81 I think we should keep synchronized block inside refreshToken method because getAuthToken is called from initCredentials as well. initCredentials is only called once before we start crawler, it doesn't need synchronization. RefreshToken is the only way to call getAuthToken in multi-thread manner and if multiple threads are trying to refreshToken at the same time we can block them there itself.

* @throws UnauthorizedException if the API returns 403 (Forbidden)
* @throws RuntimeException if all retries are exhausted or unexpected errors occur
*/
public <T> ResponseEntity<T> invokeGetApi(URI uri, Class<T> responseType) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would recommend to see the possibility of reusing existing code here. I see the current RestClient code is more inside the Atlassian commons but we can pull that out into the source crawler and make it available for any future saas sources as well.

By reusing, we will minimize the future code maintenance and make sure every plugin gets the future fixes.

I see the additional headers setup in this method which was done here in the Atlassian case. That is also generalizable.

AddressValidation validation is something that you probably don't need as you are not even asking for url from the customer. Apart from that, we can reuse rest of the logic. Lets try that here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @san81 I'll refactor this in the follow up PR.

sub = URLDecoder.decode(encodedSub, StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
log.warn("Invalid URL encoding in subfilter: {}", encodedSub);
continue;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is it Ok to continue in this case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is okay to continue here because there is no point in further validating that sub filter. We will just ignore that subfilter completely and move on to the next one. for example if url is last_updated:>=1745519529+bad%ZZsegment+_marker:<'abc'> we will ignore bad%ZZsegment and santize url last_updated:>=1745519529+_marker:<'abc'>


@Test
void testValidEncodedCrowdStrikeUrlPreserved() throws MalformedURLException {
String url = "https://api.crowdstrike.com//intel/combined/indicators/v1" +
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: double / after crowdstrike.com. probably a typo?

when(restClient.invokeGetApi(eq(sanitizedUri), eq(CrowdStrikeIndicatorResult.class)))
.thenReturn(responseEntity);

CrowdStrikeApiResponse response = service.getAllContent(null, null, paginationLink);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is null an acceptable value for startTime and endTime?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, startTime and endTime cannot be null anymore because we are passing Instant now and startTime.getEpochSeconds() with throw NPE. I'll add a null check in the method itself. Thanks for catching this.

/**
* CrowdStrike service Test
*/
class CrowdStrikeServiceTest {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See if you can add any test case to validate the searchCallLatencyTimer metric as well.

* @param paginationLink An optional pagination URL suffix (used when fetching next pages).
* @return A {@link CrowdStrikeApiResponse} containing response body and headers.
*/
public CrowdStrikeApiResponse getAllContent(Long startTime, Long endTime, String paginationLink) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor: can we rename method and append to code comments to indicate this indicator API if it's not a generic get for all api responses

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Having configurable client timeouts will help

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll consider adding this as part of source configuration along with look_back_days in follow up PR.

Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
engechas
engechas previously approved these changes Apr 25, 2025
Comment on lines +72 to +73
String filter1 = URLEncoder.encode(LAST_UPDATED + ":>=" + startTime.getEpochSecond(), StandardCharsets.UTF_8);
String filter2 = URLEncoder.encode(LAST_UPDATED + ":<" + endTime.getEpochSecond(), StandardCharsets.UTF_8);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

minor: startTimeFilter, endTimeFilter would be better names than filter1, filter2

Comment on lines +13 to +14
@Getter
@Setter
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: @Data contains both @Getter and @Setter functionality + has the added bonus of providing a toString implementation for logging

* Represents the response returned from a CrowdStrike API call.
*/
@Getter @Setter
public class CrowdStrikeApiResponse {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: can we rename this as CrowdstrikeIntelApiResponse for disambiguation purposes.. since there are other APIs we could be integrating in the future

return new URI(urlString);
} else {
// Manually construct and encode the query string
String filter1 = URLEncoder.encode(LAST_UPDATED + ":>=" + startTime.getEpochSecond(), StandardCharsets.UTF_8);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor: plz also use CONSTANTS like GREATER_THAN, LESSER_THAN and explain in code comments with an example of what the final url will look like for readability

Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
Copy link
Copy Markdown
Collaborator

@san81 san81 left a comment

Choose a reason for hiding this comment

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

Nice progress 👍

@san81 san81 merged commit 5340c3d into opensearch-project:main Apr 28, 2025
45 of 47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants