Skip to content

[Tenable IO] Update the input type from HTTPJSON to CEL. - #9466

Merged
kcreddy merged 6 commits into
elastic:mainfrom
muskan-agarwal26:tenable_io-2.9.0
May 9, 2024
Merged

[Tenable IO] Update the input type from HTTPJSON to CEL.#9466
kcreddy merged 6 commits into
elastic:mainfrom
muskan-agarwal26:tenable_io-2.9.0

Conversation

@muskan-agarwal26

Copy link
Copy Markdown
Contributor

Type of change
Bugfix

What does this PR do?

  1. Update the input type to CEL for better debugging.
  2. Added fingerprint in the plugin data stream.

Checklist

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

All changes

[x] Change follows the contributing guidelines
[x] Supported versions of the monitoring target are documented
[x] Supported operating systems are documented (if applicable)
[x] Integration or System tests exist
[x] Documentation exists
[x] Fields follow ECS and naming conventions
[x] At least a manual test with ES / Kibana / Agent has been performed.
[x] Required Kibana version set to: ^8.12.0

How to test this PR locally

Clone integrations repo.
Install the elastic package locally.
Start the elastic stack using the elastic package.
Move to integrations/packages/tenable_io directory.
Run the following command to run tests.
elastic-package test -v

Related issues

Closes #9355

Automated Test

test-tenable.log

1. Update the input type to CEL for better debugging
2. Added fingerprint in the plugin data stream
@muskan-agarwal26
muskan-agarwal26 requested a review from a team as a code owner March 28, 2024 13:33
@cla-checker-service

cla-checker-service Bot commented Mar 28, 2024

Copy link
Copy Markdown

💚 CLA has been signed

@kcreddy

kcreddy commented Apr 1, 2024

Copy link
Copy Markdown
Contributor

/test

@elastic-sonarqube

Copy link
Copy Markdown
Comment thread packages/tenable_io/changelog.yml Outdated
@@ -1,4 +1,9 @@
# newer versions go on top
- version: "2.10.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- version: "2.10.0"
- version: "3.0.0"

Lets update major

- version: "2.10.0"
changes:
- description: Changed data collection input type of all the data streams from HTTPJSON to CEL.
type: bugfix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
type: bugfix
type: enhancement
@@ -1,7 +1,7 @@
input: httpjson

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add assert.hit_count to all datastreams system test config

Comment on lines +27 to +37
(has(state.want_more) && state.want_more ? state : state.with({
"request_time":(
has(state.cursor) && has(state.cursor.last_event_ts) && state.cursor.last_event_ts != null
?
state.cursor.last_event_ts
:
int(now - duration(state.initial_interval))
)
}).as(state, state.with(
post_request(state.url + "/assets/export", "application/json",
'''{"chunk_size": ''' + state.batch_size.encode_json() + ''',"filters": {"updated_at": ''' + state.request_time.encode_json() + '''}}'''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In case we have PROCESSING export status, then state.want_more is true. This will continue and create a new export job even though the previous one is still PROCESSING. We are trying to address this issue by not creating new export job when previous one is already running. Wouldn't multiple jobs lead to corrupted state?

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've implemented a condition where, as long as the export status is PROCESSING, the state.want_more will remain true. This condition will continuously check the status in a loop until the export status is FINISHED. Consequently, no new export job will be initiated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Alright, lets say after the 1st run, you created export job uuid - 1 and checked its status which resulted in PROCESSING. So, your want_more becomes true while checking /1/status as per:

"want_more": (inner_body.?status.orValue("") != "FINISHED" || size(inner_body.chunks_available) > 0) && int(now) - int(state.export_job_start_time) <= int(state.time_value),

Then, in your next iteration, since want_more is true, it calls post_request to create a new export job again.

has(state.want_more) && state.want_more ? state : state.with({
"request_time": (
state.?cursor.last_event_ts.orValue(null) != null ?
state.cursor.last_event_ts
:
int(now - duration(state.initial_interval))
)
}).as(state, state.with(
post_request(
state.url + "/assets/export", "application/json",

I think there has to be a check if the previous job is finished or reached timeout, only then start a new export.
Has this been tested against a considerable payload where export status doesn't reach FINISHED immediately? You can try also with a system test config returning PROCESSING or QUEUED status and after reaching export_status_timeout, it creates a new export job.

@muskan-agarwal26 muskan-agarwal26 Apr 22, 2024

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.

If want_more is true based on the condition has(state.want_more) && state.want_more ? state : state.with({ , it won't run the state.with part of the code. Instead, it will proceed directly with state. So, for the as request: as(state, state.with( , it won't trigger the state.with code, meaning it won't call post_request to make a new export. Instead, it will call the /export/{export_uid}/status API.

Yes, we can provide a check if the previous job is finished or reached timeout, only then start a new export. I will add the code and update the PR soon.

"worklist": {}
}
)
))).as(state, has(state.worklist) && has(state.worklist.export_uuid) && has(state.chunk_status) && state.chunk_status.compare("FINISHED") != 0 ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

how are you handling the case where export status is stuck in PROCESSING and never reaches FINISHED? I think this is possible on Tenable side. In this case, we may want to compare when our export job started and current time and if it still stuck in PROCESSING state, then clear the state for new export job to run.

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.

What parameter shall we consider to deduce this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can calculate based on when the export job started (you can store this inside the state) and status is available from chunk_status

Comment thread packages/tenable_io/manifest.yml Outdated
name: tenable_io
title: Tenable Vulnerability Management
version: "2.9.0"
version: "2.10.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bump major

@efd6 efd6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only CEL code reviewed.

Comment on lines +27 to +113
request("GET", state.url + "/plugins/plugin?last_updated=" + (
has(state.want_more) && !state.want_more
?
(
has(state.cursor) && has(state.cursor.last_update_time) && state.cursor.last_update_time != null
?
string((state.cursor.last_update_time)).split("T")[0]
:
"1970-01-01"
)
:
(
has(state.cursor) && has(state.cursor.first_update_time) && state.cursor.first_update_time != null
?
string((state.cursor.first_update_time)).split("T")[0]
:
""
)) + "&page=" + string(state.next_page) + "&size=" + string(state.batch_size)
).with({
"Header":{
"X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
"User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/2.9.0)"]
}
}).do_request().as(resp,
resp.StatusCode == 200 ?
bytes(resp.Body).decode_json().as(body, {
"events": (has(body.data.plugin_details) && body.data.plugin_details.size() > 0 ?
(
body.data.plugin_details.map(e,{
"message": e.encode_json()
})
)
:
(
[{"message": body}]
)
),
"cursor": {
"last_update_time": (
has(body.data.plugin_details) && body.data.plugin_details.size() > 0
?
(
has(state.cursor) && has(state.cursor.last_update_time) && state.want_more
?
state.cursor.last_update_time
:
now
)
:
has(state.cursor) && has(state.cursor.last_update_time)
?
state.cursor.last_update_time
:
null
),
"first_update_time": (
has(state.cursor) && has(state.cursor.first_update_time) && state.cursor.first_update_time != null && has(body.data.plugin_details) && body.data.plugin_details.size() > 0
?
(
body.size == int(state.batch_size) && state.want_more ? state.cursor.first_update_time : state.cursor.last_update_time
)
:
"1970-01-01T00:00:00Z"
),
},
"next_page": body.size == int(state.batch_size) ? int(state.next_page)+1 : 1,
"want_more": body.size == int(state.batch_size),
"access_key": state.access_key,
"secret_key": state.secret_key,
"batch_size": state.batch_size
})
:
{
"events": {
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),
"message": string(resp.Body)
},
},
"want_more": false,
"next_page": 1,
"batch_size": state.batch_size,
"access_key": state.access_key,
"secret_key": state.secret_key
}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a clean up of indentation, use of optional types, checking for field presence and updating user-agent version. It is untested.

Suggested change
request("GET", state.url + "/plugins/plugin?last_updated=" + (
has(state.want_more) && !state.want_more
?
(
has(state.cursor) && has(state.cursor.last_update_time) && state.cursor.last_update_time != null
?
string((state.cursor.last_update_time)).split("T")[0]
:
"1970-01-01"
)
:
(
has(state.cursor) && has(state.cursor.first_update_time) && state.cursor.first_update_time != null
?
string((state.cursor.first_update_time)).split("T")[0]
:
""
)) + "&page=" + string(state.next_page) + "&size=" + string(state.batch_size)
).with({
"Header":{
"X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
"User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/2.9.0)"]
}
}).do_request().as(resp,
resp.StatusCode == 200 ?
bytes(resp.Body).decode_json().as(body, {
"events": (has(body.data.plugin_details) && body.data.plugin_details.size() > 0 ?
(
body.data.plugin_details.map(e,{
"message": e.encode_json()
})
)
:
(
[{"message": body}]
)
),
"cursor": {
"last_update_time": (
has(body.data.plugin_details) && body.data.plugin_details.size() > 0
?
(
has(state.cursor) && has(state.cursor.last_update_time) && state.want_more
?
state.cursor.last_update_time
:
now
)
:
has(state.cursor) && has(state.cursor.last_update_time)
?
state.cursor.last_update_time
:
null
),
"first_update_time": (
has(state.cursor) && has(state.cursor.first_update_time) && state.cursor.first_update_time != null && has(body.data.plugin_details) && body.data.plugin_details.size() > 0
?
(
body.size == int(state.batch_size) && state.want_more ? state.cursor.first_update_time : state.cursor.last_update_time
)
:
"1970-01-01T00:00:00Z"
),
},
"next_page": body.size == int(state.batch_size) ? int(state.next_page)+1 : 1,
"want_more": body.size == int(state.batch_size),
"access_key": state.access_key,
"secret_key": state.secret_key,
"batch_size": state.batch_size
})
:
{
"events": {
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),
"message": string(resp.Body)
},
},
"want_more": false,
"next_page": 1,
"batch_size": state.batch_size,
"access_key": state.access_key,
"secret_key": state.secret_key
}
)
request("GET", state.url + "/plugins/plugin?last_updated=" + (
has(state.want_more) && !state.want_more ?
(
has(state.?cursor.last_update_time) && state.cursor.last_update_time != null ?
string(state.cursor.last_update_time).split("T")[0]
:
"1970-01-01"
)
:
(
has(state.?cursor.first_update_time) && state.cursor.first_update_time != null ?
string((state.cursor.first_update_time)).split("T")[0]
:
""
)
) + "&page=" + string(state.next_page) + "&size=" + string(state.batch_size)
).with({
"Header":{
"X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
"User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/"3.0.0)"] // Keep build version up-to-date with package version.
}
}).do_request().as(resp,
resp.StatusCode == 200 ?
bytes(resp.Body).decode_json().as(body, {
"events": (
has(body.?data.plugin_details) && body.data.plugin_details.size() > 0 ?
body.data.plugin_details.map(e, {
"message": e.encode_json()
})
:
[{"message": body}]
),
"cursor": {
"last_update_time": (
has(body.?data.plugin_details) && body.data.plugin_details.size() > 0 ?
(
has(state.?cursor.last_update_time) && state.want_more ?
state.cursor.last_update_time
:
now
)
:
state.?cursor.last_update_time.orValue(null)
),
"first_update_time": (
has(state.?cursor.first_update_time) && state.cursor.first_update_time != null && has(body.data.plugin_details) && body.data.plugin_details.size() > 0 ?
(
body.size == int(state.batch_size) && state.want_more ?
state.cursor.first_update_time
:
state.cursor.last_update_time
)
:
"1970-01-01T00:00:00Z"
),
},
"next_page": body.size == int(state.batch_size) ? int(state.next_page)+1 : 1,
"want_more": body.size == int(state.batch_size),
"access_key": state.access_key,
"secret_key": state.secret_key,
"batch_size": state.batch_size
})
:
{
"events": {
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),
"message": string(resp.Body)
},
},
"want_more": false,
"next_page": 1,
"batch_size": state.batch_size,
"access_key": state.access_key,
"secret_key": state.secret_key
}
)
Comment on lines +24 to +58
request("GET", state.url + "/scans").with({
"Header":{
"X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
"User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/2.9.0)"]
}
}).do_request().as(resp,
resp.StatusCode == 200 ?
bytes(resp.Body).decode_json().as(body, {
"events": (has(body.scans) && body.scans.size() > 0 ?
(
body.scans.map(e, {
"message": e.encode_json()
})
)
:
(
[{"message": body}]
)
),
"access_key": state.access_key,
"secret_key": state.secret_key
})
:
{
"events": [{
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),
"message": string(resp.Body)
}
}],
"access_key": state.access_key,
"secret_key": state.secret_key
}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixes indentation and user-agent version. Untested.

Suggested change
request("GET", state.url + "/scans").with({
"Header":{
"X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
"User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/2.9.0)"]
}
}).do_request().as(resp,
resp.StatusCode == 200 ?
bytes(resp.Body).decode_json().as(body, {
"events": (has(body.scans) && body.scans.size() > 0 ?
(
body.scans.map(e, {
"message": e.encode_json()
})
)
:
(
[{"message": body}]
)
),
"access_key": state.access_key,
"secret_key": state.secret_key
})
:
{
"events": [{
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),
"message": string(resp.Body)
}
}],
"access_key": state.access_key,
"secret_key": state.secret_key
}
)
request("GET", state.url + "/scans").with({
"Header":{
"X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
"User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/3.0.0)"] // Keep up-to-date with package version.
}
}).do_request().as(resp,
resp.StatusCode == 200 ?
bytes(resp.Body).decode_json().as(body, {
"events": (
has(body.scans) && body.scans.size() > 0 ?
(
body.scans.map(e, {
"message": e.encode_json()
})
)
:
[{"message": body}]
),
"access_key": state.access_key,
"secret_key": state.secret_key
})
:
{
"events": [{
"error": {
"code": string(resp.StatusCode),
"id": string(resp.Status),
"message": string(resp.Body)
}
}],
"access_key": state.access_key,
"secret_key": state.secret_key
}
)
fields:
- access_key
- secret_key
program: |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Program suggestion (GH will not let be scroll and select lines): fixes indentation, UA version, etc. Untested

  (
    state.?want_more.orValue(false) ? state : state.with({
      "request_time": (
        has(state.?cursor.last_event_time) && state.cursor.last_event_time != null ?
          state.cursor.last_event_time
        :
          int(now - duration(state.initial_interval))
      )
    }).as(state, state.with(
      post_request(state.url + "/vulns/export", "application/json",
        '{"num_assets":' + state.batch_size.encode_json() + ',"filters":{"since":' + state.request_time.encode_json() + ',"state": ["open","reopened","fixed"]}}'
      ).with({
        "Header":{
            "X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
            "User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/3.0.0)"] // Keep up-to-date with package version.
        }
      }).do_request().as(resp,
        resp.StatusCode == 200 ?
          {
            "worklist": bytes(resp.Body).decode_json(),
            "cursor": {
              "last_event_time": int(now)
            }
          }
        :
          {
            "response_error": {
              "error": {
                "code": string(resp.StatusCode),
                "id": string(resp.Status),
                "message": string(resp.Body)
              },
            },
            "want_more": false,
            "worklist": {}
          }
        )
    ))
  ).as(state,
    has(state.?worklist.export_uuid) && state.?chunk_status.orValue("") != "FINISHED" ?
      request("GET",
        state.url +"/vulns/export/" + state.worklist.export_uuid + "/status"
      ).with({
        "Header":{
          "X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
          "User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/3.0.0)"] // Keep up-to-date with package version.
        }
      }).do_request().as(response,
        response.StatusCode == 200 ?
          bytes(response.Body).decode_json().as(inner_body, {
            "want_more": inner_body.?status.orValue("") != "FINISHED" || size(inner_body.chunks_available) > 0,
            "chunk_status": inner_body.?status.orValue("") == "FINISHED" && size(inner_body.chunks_available) == 0 ? "QUEUED" : inner_body.status,
            "worklist": state.worklist,
            "chunks": inner_body.chunks_available,
            "url": state.url,
            "next": 0,
            "access_key": state.access_key,
            "secret_key": state.secret_key,
            "initial_interval": state.initial_interval,
            "batch_size": state.batch_size,
            "cursor": {
              "last_event_time": state.?cursor.last_event_time.orValue(int(now))
            }
          })
        :
          {
            "response_error": {
              "error": {
                "code": string(response.StatusCode),
                "id": string(response.Status),
                "message": string(response.Body)
              },
            },
            "url": state.url,
            "want_more": false,
            "chunks": [],
            "chunk_status": "QUEUED",
            "worklist": {},
            "next": 0,
            "access_key": state.access_key,
            "secret_key": state.secret_key,
            "initial_interval": state.initial_interval,
            "batch_size": state.batch_size,
          }
      )
    :
      {
        "url": state.url,
        "want_more": state.want_more,
        "chunks": state.?chunks.orValue([]),
        "chunk_status": state.chunk_status,
        "worklist": state.?worklist.orValue({}),
        "next": state.?next.orValue(0),
        "access_key": state.access_key,
        "secret_key": state.secret_key,
        "initial_interval": state.initial_interval,
        "batch_size": state.batch_size,
        "response_error": state.?response_error.orValue({}),
        "cursor": {
          "last_event_time": state.?cursor.last_event_time.orValue(int(now))
        }
      }
  ).as(state,
    has(state.?worklist.export_uuid) && state.?chunk_status.orValue("") == "FINISHED" && size(state.chunks) > 0 ?
      request("GET",
        state.url +"/vulns/export/" + state.worklist.export_uuid + "/chunks/" + string(state.chunks[state.next])
      ).with({
        "Header":{
          "X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
          "User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/3.0.0)"] // Keep up-to-date with package version.
        }
      }).do_request().as(response1,
          response1.StatusCode == 200 ?
            bytes(response1.Body).decode_json().as(second_chain_body, {
              "events": (
                second_chain_body != null && size(second_chain_body) > 0 ?
                  second_chain_body.map(e, {
                    "message": e.encode_json()
                  })
                :
                  [{}]
              ),
              "want_more": (int(state.next)+1) < size(state.chunks),
              "worklist": (int(state.next)+1) < size(state.chunks) ? state.worklist : {},
              "next": (int(state.next)+1) < size(state.chunks) ? (int(state.next)+1) : 0,
              "chunk_status": ((int(state.next)+1) < size(state.chunks)) || (second_chain_body == null) ? state.chunk_status : "QUEUED",
              "access_key": state.access_key,
              "secret_key": state.secret_key,
              "initial_interval": state.initial_interval,
              "batch_size": state.batch_size,
              "chunks": (int(state.next)+1) < size(state.chunks) ? state.chunks : [],
              "url": state.url,
              "cursor": {
                "last_event_time": state.?cursor.last_event_time.orValue(int(now))
              }
            })
          :
            {
              "events": {
                "error": {
                  "code": string(response1.StatusCode),
                  "id": string(response1.Status),
                  "message": string(response1.Body)
                },
              },
              "url": state.url,
              "want_more": false,
              "chunks": [],
              "chunk_status": "QUEUED",
              "worklist": {},
              "next": 0,
              "access_key": state.access_key,
              "secret_key": state.secret_key,
              "initial_interval": state.initial_interval,
              "batch_size": state.batch_size,
            }
      )
    :
      {
        "events": state.?response_error.orValue(null) != null ? state.response_error : [{}],
        "url": state.url,
        "chunks": has(state.chunks) ? state.chunks : [],
        "chunk_status": state.chunk_status,
        "want_more": state.want_more,
        "worklist": has(state.worklist) ? state.worklist : {},
        "next": has(state.next) ? state.next : 0,
        "access_key": state.access_key,
        "secret_key": state.secret_key,
        "initial_interval": state.initial_interval,
        "batch_size": state.batch_size,
        "cursor": {
          "last_event_time": state.?cursor.last_event_time.orValue(int(now))
        }
      }
  )
fields:
- access_key
- secret_key
program: |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Program suggestion. Untested. (Does not take into account comments by @kcreddy)

  (
    has(state.want_more) && state.want_more ? state : state.with({
      "request_time": (
        state.?cursor.last_event_ts.orValue(null) != null ?
          state.cursor.last_event_ts
        :
          int(now - duration(state.initial_interval))
      )
    }).as(state, state.with(
      post_request(
        state.url + "/assets/export", "application/json",
        '{"chunk_size":' + state.batch_size.encode_json() + ',"filters":{"updated_at": ' + state.request_time.encode_json() + '}}'
      ).with({
        "Header":{
          "X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
          "User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/3.0.0)"] // Keep up-to-date with package version.
        }
      }).do_request().as(resp,
        resp.StatusCode == 200 ?
          {
            "worklist": bytes(resp.Body).decode_json(),
            "cursor": {
              "last_event_ts": int(now)
            }
          }
        :
          {
            "response_error": {
              "error": {
                "code": string(resp.StatusCode),
                "id": string(resp.Status),
                "message": string(resp.Body)
              },
            },
            "want_more": false,
            "worklist": {}
          }
      )
    ))
  ).as(state, 
    has(state.?worklist.export_uuid) && state.?chunk_status.compare.orValue("") != "FINISHED" ?
      request("GET",
        state.url +"/assets/export/" + state.worklist.export_uuid + "/status"
      ).with({
        "Header":{
          "X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
          "User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/3.0.0)"] // Keep up-to-date with package version.
        }
      }).do_request().as(response,
        response.StatusCode == 200 ?
          bytes(response.Body).decode_json().as(inner_body, {
            "want_more": inner_body.?status.orValue("") != "FINISHED" || size(inner_body.chunks_available) > 0,
            "chunk_status": inner_body.?status.orValue("") == "FINISHED" && size(inner_body.chunks_available) == 0 ? "QUEUED" : inner_body.status,
            "worklist": state.worklist,
            "chunks": inner_body.chunks_available,
            "url": state.url,
            "next": 0,
            "access_key": state.access_key,
            "secret_key": state.secret_key,
            "initial_interval": state.initial_interval,
            "batch_size": state.batch_size,
            "cursor": {
              "last_event_ts": state.?cursor.last_event_ts.orValue(int(now))
            }
          })
        :
          {
            "response_error": {
              "error": {
                "code": string(response.StatusCode),
                "id": string(response.Status),
                "message": string(response.Body)
              },
            },
            "url": state.url,
            "want_more": false,
            "chunks": [],
            "chunk_status": "QUEUED",
            "worklist": {},
            "next": 0,
            "access_key": state.access_key,
            "secret_key": state.secret_key,
            "initial_interval": state.initial_interval,
            "batch_size": state.batch_size,
          }
      )
    :
      {
        "url": state.url,
        "want_more": state.want_more,
        "chunks": has(state.chunks) ? state.chunks : [],
        "chunk_status": state.chunk_status,
        "worklist": has(state.worklist) ? state.worklist : {},
        "next": has(state.next) ? state.next : 0,
        "access_key": state.access_key,
        "secret_key": state.secret_key,
        "initial_interval": state.initial_interval,
        "batch_size": state.batch_size,
        "response_error": has(state.response_error) ? state.response_error : {},
        "cursor": {
          "last_event_ts": (
            has(state.cursor) && has(state.cursor.last_event_ts)
            ?
              state.cursor.last_event_ts
            :
              int(now)
          )
        }
      }
  ).as(state,
    has(state.?worklist.export_uuid) && state.?chunk_status.orValue("") == "FINISHED" && size(state.chunks) > 0 ?
      request("GET",
        state.url +"/assets/export/" + state.worklist.export_uuid + "/chunks/" + string(state.chunks[state.next])
      ).with({
        "Header":{
          "X-ApiKeys": ["accessKey=" + state.access_key + ";secretKey=" + state.secret_key],
          "User-Agent": ["Integration/1.0 (Elastic; Tenable.io; Build/3.0.0)"] // Keep up-to-date with package version.
        }
      }).do_request().as(response1,
        response1.StatusCode == 200 ?
          bytes(response1.Body).decode_json().as(second_chain_body, {
            "events": (second_chain_body != null && size(second_chain_body) > 0 ?
              second_chain_body.map(e,{
                "message": e.encode_json()
              })
            :
              [{}]
            ),
            "want_more": (int(state.next)+1) < size(state.chunks),
            "worklist": (int(state.next)+1) < size(state.chunks) ? state.worklist : {},
            "next": (int(state.next)+1) < size(state.chunks) ? (int(state.next)+1) : 0,
            "chunk_status": ((int(state.next)+1) < size(state.chunks)) || (second_chain_body == null) ? state.chunk_status : "QUEUED",
            "access_key": state.access_key,
            "secret_key": state.secret_key,
            "initial_interval": state.initial_interval,
            "batch_size": state.batch_size,
            "chunks": (int(state.next)+1) < size(state.chunks) ? state.chunks : [],
            "url": state.url,
            "cursor": {
              "last_event_ts": state.?cursor.last_event_ts.orValue(int(now))
            }
          })
        :
          {
            "events": {
              "error": {
                "code": string(response1.StatusCode),
                "id": string(response1.Status),
                "message": string(response1.Body)
              },
            },
            "url": state.url,
            "want_more": false,
            "chunks": [],
            "chunk_status": "QUEUED",
            "worklist": {},
            "next": 0,
            "access_key": state.access_key,
            "secret_key": state.secret_key,
            "initial_interval": state.initial_interval,
            "batch_size": state.batch_size,
          }
      )
    :
      {
        "events": state.?response_error.orValue(null) != null ? state.response_error : [{}],
        "url": state.url,
        "chunks": state.?chunks.orValue([]),
        "chunk_status": state.chunk_status,
        "want_more": state.want_more,
        "worklist": state.?worklist.orValue({}),
        "next": state.?next.orValue(0),
        "access_key": state.access_key,
        "secret_key": state.secret_key,
        "initial_interval": state.initial_interval,
        "batch_size": state.batch_size,
        "cursor": {
          "last_event_ts": state.?cursor.last_event_ts.orValue(int(now))
        }
      }
  )
1. Update the build version 3.0.0 and change the type to enhancement
2. Added asset.hit_count to all datastreams system test config
3. Handled the export_status timeout condition in assets and vulnerability datastreams
4. Applied the indentation as suggested
@kcreddy

kcreddy commented Apr 18, 2024

Copy link
Copy Markdown
Contributor

I added my comment here - #9466 (comment)

Essentially we are trying to solve both problems noted in the original issue by migrating httpjson to CEL: #9355 (comment) which I don't think CEL code is currently doing. Please add system tests to simulate both the conditions (or) atleast test both conditions on real data on test instance.

@efd6 efd6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with @kcreddy in #9466 (comment). The issues here are subtle enough and the code complex enough that this warrants having a test to show that it is fixed. If this cannot be done with the current tools provided by stream, then we need to make that possible.


There are some minimum requirements for running Elastic Agent and for more information, refer to the link [here](https://www.elastic.co/guide/en/fleet/current/elastic-agent-installation.html).

The minimum **kibana.version** required is **8.7.1**.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
The minimum **kibana.version** required is **8.7.1**.
The minimum **kibana.version** required is **8.12.0**.
Comment on lines +3 to +6
changes:
- description: Changed data collection input type of all the data streams from HTTPJSON to CEL.
type: enhancement
link: https://github.com/elastic/integrations/pull/9466

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should include the bugfixes in the changes as well, as separate elements in the changes list.

Comment on lines +51 to +53
"time_value": string(state.export_status_timeout).has_suffix("s") ? string(state.export_status_timeout).split("s")[0]
: string(state.export_status_timeout).has_suffix("h") ? string(int(string(state.export_status_timeout).split("h")[0]) * 3600)
: string(state.export_status_timeout).has_suffix("m") ? string(int(string(state.export_status_timeout).split("m")[0]) * 60)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What are these for? The export status timeout field is defined in the documentation to match the time.Duration format and CEL's duration type natively parses this format.

Type conversion. Duration strings should support the following suffixes: "h" (hour), "m" (minute), "s" (second), "ms" (millisecond), "us" (microsecond), and "ns" (nanosecond). Duration strings may be zero, negative, fractional, and/or compound. Examples: "0", "-1.5h", "1m6s"

I'd also say that "time_value" is not an informative name; it should refer to the function rather than the type (which I think is also wrong, since it's not a time, but a duration).

Suggest making this an actual time by adding the export_status_timeout to now into "expires" and then comparing now against that to evaluate whether the request is too old.

Comment on lines +25 to +27
- name: export_status_timeout
type: text
title: Minimum Wait Time
title: Export Status Timeout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the "Status" part of this meaningful to users? I don't think so. Suggest "Export Request Timeout".

@muskan-agarwal26 muskan-agarwal26 Apr 22, 2024

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.

We've set up a way to check if the /export/{export_uuid}/status API has returned FINISHED in status. If it's not finished, we'll stop the process after a certain time (user configurable). After that, we won't keep checking the export status. So, the export_status_timeout variable represents this check for the /export/{export_uuid}/status API.

:
(
has(state.?cursor.first_update_time) && state.cursor.first_update_time != null ?
string((state.cursor.first_update_time)).split("T")[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've seen this .split("T")[0] in a number of places but I do not understand why it is being used; from the documentation for the various APIs that have CEL programs with this idiom, they use RFC3339 formatted time. string(timestamp) returns this, so it is surprising that this additional step is taken. Can you explain why it is needed, otherwise, please remove it.

Also, we don't need the parens around the field access.

Suggested change
string((state.cursor.first_update_time)).split("T")[0]
string(state.cursor.first_update_time).split("T")[0]
request("GET", state.url + "/plugins/plugin?last_updated=" + (
has(state.want_more) && !state.want_more ?
(
has(state.?cursor.last_update_time) && state.cursor.last_update_time != null ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
has(state.?cursor.last_update_time) && state.cursor.last_update_time != null ?
state.?cursor.last_update_time.orValue(null) != null ?

(similar throughout)

- access_key
- secret_key
program: |
request("GET", state.url + "/plugins/plugin?last_updated=" + (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is unsafe either without documentation or additonal string handling; if the user enters a URL with a trailing slash (some environments will add this to the clipboard without user intervention) then we have a URL with a double slash separating the path from the host. This breaks some things. Either add as a note in the docs that the base URL entered in to the UI should not have trailing slash, or (preferably) do

Suggested change
request("GET", state.url + "/plugins/plugin?last_updated=" + (
request("GET", state.url.trim_right("/") + "/plugins/plugin?last_updated=" + (

(throughout)

(
state.?want_more.orValue(false) ? state : state.with({
"request_time": (
has(state.?cursor.last_event_time) && state.cursor.last_event_time != null ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
has(state.?cursor.last_event_time) && state.cursor.last_event_time != null ?
state.?cursor.last_event_time.orValue(null) != null ?
Comment on lines +50 to +52
"time_value": string(state.export_status_timeout).has_suffix("s") ? string(state.export_status_timeout).split("s")[0]
: string(state.export_status_timeout).has_suffix("h") ? string(int(string(state.export_status_timeout).split("h")[0]) * 3600)
: string(state.export_status_timeout).has_suffix("m") ? string(int(string(state.export_status_timeout).split("m")[0]) * 60)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same comment as above.

}).do_request().as(resp,
resp.StatusCode == 200 ?
{
"worklist": bytes(resp.Body).decode_json(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To clarify, this doesn't look like a worklist to me, but rather a next item to collect. Is that correct? It always holds an object and never an array. Can we change this to a less misleading name?

@narph narph added the Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations] label Apr 26, 2024
@elasticmachine

Copy link
Copy Markdown

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

@narph narph added the Crest Contributions from Crest developement team. label Apr 26, 2024
1. Implemented the cancel export_uid API to terminate the export job ID if the timeout condition is met, in asset and vulnerability data stream.
2. Added a check to ensure that if the previous export is not finished, a new export shouldn't be initiated, in asset and vulnerability data stream.
3. Added bugfix in changelog lists.
@muskan-agarwal26
muskan-agarwal26 requested review from efd6 and kcreddy May 8, 2024 13:36
@efd6

efd6 commented May 8, 2024

Copy link
Copy Markdown
Contributor

/test

@elasticmachine

Copy link
Copy Markdown

🚀 Benchmarks report

To see the full report comment with /test benchmark fullreport

@efd6 efd6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This LGTM now. I have done some final indentation fixups which can be viewed in the final commit; best viewed with hide whitespace where you'll see only -2 +4 changes which are line diff whitespacing. Approving, but wait for @kcreddy.

@efd6

efd6 commented May 9, 2024

Copy link
Copy Markdown
Contributor

/test

@elasticmachine

Copy link
Copy Markdown

💚 Build Succeeded

History

@kcreddy kcreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 👍🏼

@kcreddy kcreddy added Integration:tenable_io Tenable Vulnerability Management enhancement New feature or request labels May 9, 2024
@kcreddy
kcreddy merged commit 97f929f into elastic:main May 9, 2024
@elasticmachine

Copy link
Copy Markdown

Package tenable_io - 3.0.0 containing this change is available at https://epr.elastic.co/search?package=tenable_io

@elasticmachine

Copy link
Copy Markdown

Package tenable_io - 3.0.0 containing this change is available at https://epr.elastic.co/search?package=tenable_io

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

Labels

Crest Contributions from Crest developement team. enhancement New feature or request Integration:tenable_io Tenable Vulnerability Management Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations]

5 participants