class CloudBuild:
    def __init__(self, trigger_name):
        credentials, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
        self.authed_session = AuthorizedSession(credentials)
        cloud_build_triggers_url = f"https://cloudbuild.googleapis.com/v1/projects/{PROJECT_ID}/triggers"
        cloud_build_triggers = self.authed_session.request("GET", cloud_build_triggers_url)
        self.cloud_build_trigger_id = ""
        for cloud_build_trigger in cloud_build_triggers.json()["triggers"]:
            if cloud_build_trigger["name"] == trigger_name:
                self.cloud_build_trigger_id = cloud_build_trigger["id"]

    def run_trigger(self):
        cloud_build_trigger_url \
            = f"https://cloudbuild.googleapis.com/v1/projects/{PROJECT_ID}/triggers/{self.cloud_build_trigger_id}:run"
        request_body = {
            "branchName": BRANCH_NAME
        }
        self.authed_session.request("POST", cloud_build_trigger_url,
                                    json.dumps(request_body), {"Content-Type": "application/json"})

    def latest_build_success(self):
        cloud_builds_url = f"https://cloudbuild.googleapis.com/v1/projects/{PROJECT_ID}/builds"
        cloud_builds = self.authed_session.request("GET", cloud_builds_url)
        for cloud_build in cloud_builds.json()['builds']:
            if cloud_build['buildTriggerId'] == self.cloud_build_trigger_id:
                return True if cloud_build['status'] == 'SUCCESS' else False
        return False
Exemple #2
0
def create_entities(http_session: requests.AuthorizedSession, customer_id: str,
                    log_type: str, json_entities: str) -> None:
    """Sends a collection of Entity data to the Chronicle backend for ingestion.

  An Entity is a structured representation of a real-world object like user,
  device etc. regardless of the log source.

  Args:
    http_session: Authorized session for HTTP requests.
    customer_id: A string containing the UUID for the Chronicle customer.
    log_type: A string containing the log type.
    json_entities: A collection of Entity protos in (serialized) JSON format.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{INGESTION_API_BASE_URL}/v2/entities:batchCreate"
    body = {
        "customer_id": customer_id,
        "log_type": log_type,
        "entities": json.loads(json_entities),
    }

    response = http_session.request("POST", url, json=body)
    response.raise_for_status()
Exemple #3
0
def iterate_spreadsheets(session: AuthorizedSession):
    page_token = ""
    url = "https://www.googleapis.com/drive/v3/files"

    params = {
        "q": "mimeType='application/vnd.google-apps.spreadsheet'",
        "pageSize": 1000,
        "supportsTeamDrives": True,
        "includeTeamDriveItems": True,
        "fields": "nextPageToken,files(name,id)",
    }

    while page_token is not None:
        if page_token:
            params["pageToken"] = page_token

        res = session.request("get", url, params=params)
        assert res.status_code == 200, res.status_code
        data = res.json()
        for file in data.get("files", []):
            yield file

        page_token = data.get("nextPageToken", None)
        if page_token is None:
            break
Exemple #4
0
def create_rule(http_session: requests.AuthorizedSession,
                rule_content: str) -> str:
    """Creates a new detection rule to find matches in logs.

  Args:
    http_session: Authorized session for HTTP requests.
    rule_content: Content of the new detection rule, used to evaluate logs.

  Returns:
    Unique ID of the new detection rule ("ru_<UUID>").

  Raises:
    ValueError: Invalid input value.
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    if not rule_content:
        raise ValueError("Missing detection rule content.")

    url = f"{CHRONICLE_API_BASE_URL}/v1/rules"
    body = {"rule": rule_content}

    response = http_session.request("POST", url, json=body)
    # Expected server response:
    # {
    #   "ruleId": "ru_<UUID>",
    #   "rule": "<rule_content>"
    # }

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()["ruleId"]
def delete_operation(http_session: requests.AuthorizedSession,
                     operation_id: str):
    """Deletes a specific asynchronous detection operation.

  This indicates that the client is no longer interested in the operation's
  result. Deletion includes an implicit and internal call to cancel_operation(),
  but unlike cancellation you can no longer query the details and latest state
  of an operation after calling delete_operation().

  Args:
    http_session: Authorized session for HTTP requests.
    operation_id: Unique ID of the asynchronous detection operation to delete
      ("rulejob_jo_<UUID>").

  Raises:
    ValueError: Invalid input value.
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    if not OPERATION_ID_PATTERN.fullmatch(operation_id):
        raise ValueError(
            f"Invalid detection operation ID: '{operation_id}' != " +
            "'rulejob_jo_<UUID>'.")

    url = f"{CHRONICLE_API_BASE_URL}/v1/operations/{operation_id}"
    response = http_session.request("DELETE", url)
    # Expected server response:
    # {}

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
Exemple #6
0
def get_alert(http_session: requests.AuthorizedSession,
              uppercase_alert_id: str) -> Mapping[str, Sequence[Any]]:
    """Retrieves the content of a specific Uppercase alert.

  Args:
    http_session: Authorized session for HTTP requests.
    uppercase_alert_id: The Uppercase alert ID.

  Returns:
    {
      "alertId": "...",
      "createTime": "...",
      "updateTime": "...",
      "eventGroups": [
        ...One or more event groups...
      ],
      "resolutionState": { ...Metadata... },
    }

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
    (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v1/uppercaseAlerts/{uppercase_alert_id}"
    response = http_session.request("GET", url)

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()
Exemple #7
0
def run():
    import logging

    logging.basicConfig(level=logging.DEBUG)

    c = Config()
    credentials = Flow(c).get_credentials(scopes=None)
    session = AuthorizedSession(credentials)

    page_token = ""
    url = "https://www.googleapis.com/drive/v3/files"

    params = {
        "q": "mimeType='application/vnd.google-apps.spreadsheet'",
        "pageSize": 1000,
        "supportsTeamDrives": True,
        "includeTeamDriveItems": True,
        "fields": "nextPageToken,files(name,id)",
    }

    while page_token is not None:
        if page_token:
            params["pageToken"] = page_token

        res = session.request("get", url, params=params)
        assert res.status_code == 200, res.status_code
        data = res.json()
        for file in data.get("files", []):
            print(f'Found file: {file.get("name")} ({file.get("id")})')

        page_token = data.get("nextPageToken", None)
        if page_token is None:
            break
def update_gcp_settings(http_session: requests.AuthorizedSession,
                        organization_id: int, enable_ingestion: bool) -> None:
  """Updates GCP settings with Chronicle.

  Args:
    http_session: Authorized session for HTTP requests.
    organization_id: GCP organization ID for the settings.
    enable_ingestion: Whether to enable GCP log ingestion or not.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
  name = f"organizations/{organization_id}/gcpAssociations/{organization_id}/gcpSettings"
  url = f"{SERVICE_MANAGEMENT_API_BASE_URL}/v1/{name}"

  body = {
      "log_flow": "LOG_FLOW_INACTIVE",
  }
  if enable_ingestion:
    body["log_flow"] = "LOG_FLOW_ACTIVE"

  response = http_session.request("PATCH", url, json=body)

  if response.status_code >= 400:
    print(response.text)
  response.raise_for_status()
Exemple #9
0
def verify_rule(http_session: requests.AuthorizedSession,
                rule_content: str) -> Mapping[str, Any]:
    """Validates that a detection rule is a valid YL2 rule.

  Args:
    http_session: Authorized session for HTTP requests.
    rule_content: Content of the detection rule.

  Returns:
    A compilation error if there is one.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules:verifyRule"
    body = {"rule_text": rule_content}

    response = http_session.request("POST", url, json=body)
    # Expected server response:
    # {
    #   "compilationError": "<error_message>", <-- IFF compilation failed.
    # }

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()
Exemple #10
0
def update_rule(http_session: requests.AuthorizedSession,
                existing_rule_id: str, new_rule_content: str):
    """Modifies the content of an existing detection rule.

  Args:
    http_session: Authorized session for HTTP requests.
    existing_rule_id: Unique ID of an existing detection rule ("ru_<UUID>").
    new_rule_content: New content for that detection rule.

  Raises:
    ValueError: Invalid input value.
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    if not RULE_ID_PATTERN.fullmatch(existing_rule_id):
        raise ValueError(
            f"Invalid detection rule ID: '{existing_rule_id}' != 'ru_<UUID>'.")
    if not new_rule_content:
        raise ValueError("Missing detection rule content.")

    url = (f"{CHRONICLE_API_BASE_URL}/v1/rules/{existing_rule_id}" +
           "?update_mask.paths=rule.rule")
    body = {"rule": new_rule_content}

    response = http_session.request("PATCH", url, json=body)
    # Expected server response:
    # {
    #   "ruleId": "ru_<UUID>",
    #   "rule": "<rule_content>"
    # }

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
Exemple #11
0
def get_list(http_session: requests.AuthorizedSession,
             name: str) -> Sequence[str]:
    """Retrieves a list.

  Args:
    http_session: Authorized session for HTTP requests.
    name: Unique name for the list.

  Returns:
    Array containing each line of the list's content.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v2/lists/{name}"

    response = http_session.request("GET", url)
    # Expected server response:
    # {
    #   "name": "<list name>",
    #   "description": "<list description>",
    #   "createTime": "yyyy-mm-ddThh:mm:ss.ssssssZ",
    #   "lines": [
    #     "<line 1>",
    #     "<line 2>",
    #     ...
    #   ]
    # }

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()["lines"]
def create_okta_user_context_feed(http_session: requests.AuthorizedSession,
                                  secret: str,
                                  hostname: str) -> Mapping[str, Any]:
  """Creates a new Okta User Context feed.

  Args:
    http_session: Authorized session for HTTP requests.
    secret: A string which represents Okta auth user's secret.
    hostname: A string which represents hostname to connect to.

  Returns:
    New Okta Feed.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
  url = f"{CHRONICLE_API_BASE_URL}/v1/feeds/"
  body = {
      "details": {
          "feedSourceType": "API",
          "logType": "OKTA_USER_CONTEXT",
          "oktaUserContextSettings": {
              "authentication": {
                  "headerKeyValues": [{
                      "key": "Authorization",
                      "value": secret
                  }]
              },
              "hostname": hostname
          }
      }
  }

  response = http_session.request("POST", url, json=body)
  # Expected server response:
  # {
  #   "name": "feeds/7c420442-6b73-439e-ae8b-563618b8fc71",
  #   "details": {
  #     "logType": "OKTA_USER_CONTEXT",
  #     "feedSourceType": "API",
  #     "oktaUserContextSettings": {
  #       "authentication": {
  #         "headerKeyValues": [
  #           {
  #             "key": "Authorization",
  #             "value": "secret_example"
  #           }
  #         ]
  #       },
  #       "hostname": "hostname_example"
  #     }
  #   },
  #   "feedState": "PENDING_ENABLEMENT"
  # }

  if response.status_code >= 400:
    print(response.text)
  response.raise_for_status()
  return response.json()
def update_gcp_log_flow_filter(http_session: requests.AuthorizedSession,
                               organization_id: int, filter_id: str,
                               filter_expression: str) -> None:
    """Updates GCP log flow filter for the given GCP organization.

  Args:
    http_session: Authorized session for HTTP requests.
    organization_id: GCP organization ID that the filter belongs to.
    filter_id: UUID for the filter.
    filter_expression: New filter expression (syntax:
      https://cloud.google.com/logging/docs/view/advanced-queries#advanced_logs_query_syntax).
        `log_id("dns.googleapis.com/dns_queries")` is an example.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    name = f"organizations/{organization_id}/gcpAssociations/{organization_id}/gcpLogFlowFilters/{filter_id}"
    url = f"{SERVICE_MANAGEMENT_API_BASE_URL}/v1/{name}"

    body = {
        "filter": filter_expression,
        # Different states are allowed (e.g., FILTER_STATE_DISABLED).
        "state": "FILTER_STATE_ENABLED",
    }

    response = http_session.request("PATCH", url, json=body)

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
def archive_rule(http_session: requests.AuthorizedSession, version_id: str):
    """Archives a detection rule.

  Archiving a rule will fail if:
  - The provided version is not the latest rule version
  - The rule is enabled as live
  - The rule has retrohunts in progress

  If alerting is enabled for a rule, archiving the rule will automatically
  disable alerting for the rule.

  Args:
    http_session: Authorized session for HTTP requests.
    version_id: Unique ID of the detection rule to archive ("ru_<UUID>" or
      "ru_<UUID>@v_<seconds>_<nanoseconds>"). If a version suffix isn't
      specified we use the rule's latest version.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{version_id}:archive"

    response = http_session.request("POST", url)
    # Expected server response:
    # {}

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
def create_gcp_association(http_session: requests.AuthorizedSession,
                           organization_id: int, nonce: str) -> None:
    """Creates a GCP association with Chronicle.

  Args:
    http_session: Authorized session for HTTP requests.
    organization_id: GCP organization ID for the association.
    nonce: 64-byte access code from a Chronicle customer representative.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    name = f"organizations/{organization_id}/gcpAssociations/{organization_id}"
    url = f"{SERVICE_MANAGEMENT_API_BASE_URL}/v1/{name}"

    body = {
        "nonce": nonce,
        "name": name,
    }

    response = http_session.request("POST", url, json=body)

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
def get_feed(http_session: requests.AuthorizedSession,
             name: str) -> Mapping[str, Any]:
    """Retrieves a feed.

  Args:
    http_session: Authorized session for HTTP requests.
    name: Unique name for the feed.

  Returns:
    Array containing each line of the feed's content.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v1/feeds/{name}"

    response = http_session.request("GET", url)
    # Expected server response:
    # {
    #   "name": "<feed name>",
    #   "feedDetails": {
    #     feedType: "...",
    #     ...settings...
    #   },
    #   "feedState": "ACTIVE / INACTIVE / PENDING_ENABLEMENT",
    # }

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()
Exemple #17
0
def unarchive_rule(http_session: requests.AuthorizedSession, version_id: str):
  """Unarchives a detection rule.

  Unarchiving a rule will fail if the provided version is not the latest rule
  version.

  Args:
    http_session: Authorized session for HTTP requests.
    version_id: Unique ID of the detection rule to unarchive ("ru_<UUID>" or
      "ru_<UUID>@v_<seconds>_<nanoseconds>"). If a version suffix isn't
      specified we use the rule's latest version.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
  url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{version_id}:unarchive"

  response = http_session.request("POST", url)
  # Expected server response:
  # {}

  if response.status_code >= 400:
    print(response.text)
  response.raise_for_status()
def list_alerts(http_session: requests.AuthorizedSession,
                page_size: Optional[int] = 100,
                page_token: str = "") -> Sequence[str]:
  """Lists up to 100 Uppercase alerts.

  Args:
    http_session: Authorized session for HTTP requests.
    page_size: Maximum number of rules to return. Must be non-negative. Optional
    - a server-side default of 100 is used if the size is 0 or a None value.
    page_token: Page token from a previous ListAlerts call used for pagination.
      Optional - the first page is retrieved if the token is the empty string or
      a None value.

  Returns:
    List of Uppercase alerts and a page token for the next page of alerts, only
    populated if there are more to fetch.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
    (response.status_code >= 400).
  """
  url = f"{CHRONICLE_API_BASE_URL}/v1/uppercaseAlerts"
  params = {"page_token": page_token, "page_size": page_size}
  response = http_session.request("GET", url, params=params)

  if response.status_code >= 400:
    print(response.text)
  response.raise_for_status()
  j = response.json()
  return j.get("uppercaseAlerts", []), j.get("nextPageToken", "")
Exemple #19
0
class ClientHelper(object):
    def __init__(self, credentials):
        self.client = AuthorizedSession(credentials)

    def make_request(self, method, url, projectId, body):

        if projectId is not None:
            url = url.replace("%projectId", projectId)

        if body is not None:
            body = json.dumps(body)
        val = self.client.request(method=method, url=url, data=body)
        val = self.__get_processed_response(val)
        return val

    def __get_processed_response(self, resp):
        respTemplate = {
            "isError": False,
            "errorMessage": None,
            "defaultErrorObject": None,
            "data": None
        }

        if resp.status_code >= 200 and resp.status_code <= 299:
            respTemplate["data"] = json.loads(resp.text)
            return respTemplate
        else:
            path = str(resp.url)
            respTemplate["defaultErrorObject"] = {
                "error": "Status Code: " + str(resp.status_code) + " Body: " + resp.text,
                "resource": path
            }
            respTemplate['isError'] = True
            return respTemplate
Exemple #20
0
def enable_live_rule(http_session: requests.AuthorizedSession, rule_id: str):
    """Enables a detection rule as live.

  The rule will run continuously against all *new* logs that your organization
  has that fall after the time the rule was enabled as live.

  To stop the rule from running, you can call the corresponding
  "disable_live_rule" action.

  If a version of a detection rule is enabled as live, then is updated with a
  new version, the following happens automatically:
  - The old version is disabled.
  - The new version is enabled as live.

  Args:
    http_session: Authorized session for HTTP requests.
    rule_id: Unique ID of the detection rule to enable ("ru_<UUID>"). A version
      suffix should not be provided, because at most one version of a detection
      rule (by default the latest version of a rule) can be enabled at a time.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{rule_id}:enableLiveRule"
    response = http_session.request("POST", url)
    # Expected server response:
    # {}

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
def create_logs(http_session: requests.AuthorizedSession, log_type: str,
                customer_id: str, logs_text: str) -> None:
    """Sends unstructured log entries to the Chronicle backend for ingestion.

  Args:
    http_session: Authorized session for HTTP requests.
    log_type: Log type for a feed. To see supported log types run
      list_supported_log_types.py
    customer_id: A string containing the UUID for the Chronicle customer.
    logs_text: A string containing logs delimited by new line characters. The
      total size of this string may not exceed 1MB or the resultsing request to
      the ingestion API will fail.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{INGESTION_API_BASE_URL}/v2/unstructuredlogentries:batchCreate"
    entries = [{"logText": t} for t in logs_text.splitlines()]
    body = {
        "customerId": customer_id,
        "logType": log_type,
        "entries": entries,
    }

    response = http_session.request("POST", url, json=body)
    response.raise_for_status()
def list_structured_query_events(
        http_session: requests.AuthorizedSession,
        raw_query: str,
        start_time: datetime.datetime,
        end_time: datetime.datetime,
        page_size: Optional[int] = 100000) -> Mapping[str, Sequence[Any]]:
    """Lists up to 10,000 UDM events that match the query.

  If you receive the maximum number of results, there might still be more
  discovered within the specified time range. You might want to narrow the time
  range and issue the call again to ensure you have visibility on the results.

  You can use the API call ListStructuredQueryEvents to search for UDM events
  by UDM field.

  Args:
    http_session: Authorized session for HTTP requests.
    raw_query: Query for searching UDM events.
    start_time: Inclusive beginning of the time range of events to return, with
      any timezone (even a timezone-unaware datetime object, i.e. local time).
    end_time: The exclusive end of the time range of events to return, with any
      timezone (even a timezone-unaware datetime object, i.e. local time).
    page_size: Maximum number of events to return, up to 10,000 (default =
      10,000).

  Returns:
    {
      "results": [
        {
          "event": "..." <-- UDM event
          "eventLogToken": "..."
        },
        ...More events...
      ],
      "moreDataAvailable": true
      "runtimeErrors": [
        {
          "errorText": "..."
        },
        ...More errors...
      ]
    }

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
    (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v1/events/liststructuredqueryevents"
    params = {
        "raw_query": raw_query,
        "start_time": datetime_converter.strftime(start_time),
        "end_time": datetime_converter.strftime(end_time),
        "page_size": page_size
    }
    response = http_session.request("GET", url, params=params)

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()
Exemple #23
0
def get_rule(http_session: requests.AuthorizedSession, rule_id: str) -> str:
    """Retrieves the content of a specific detection rule.

  Args:
    http_session: Authorized session for HTTP requests.
    rule_id: Unique ID of the detection rule to retrieve ("ru_<UUID>").

  Returns:
    Content of the requested detection rule, which is used to evaluate logs.

  Raises:
    ValueError: Invalid input value.
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    if not RULE_ID_PATTERN.fullmatch(rule_id):
        raise ValueError(
            f"Invalid detection rule ID: '{rule_id}' != 'ru_<UUID>'.")

    url = f"{CHRONICLE_API_BASE_URL}/v1/rules/{rule_id}"
    response = http_session.request("GET", url)
    # Expected server response:
    # {
    #   "ruleId": "ru_<UUID>",
    #   "rule": "<rule_content>"
    # }

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()["rule"]
def disable_live_rule(http_session: requests.AuthorizedSession, rule_id: str):
    """Disables a detection rule that is currently enabled as live.

  If a version of a detection rule is enabled as live, then is updated with a
  new version, the following happens automatically:
  - The old version is disabled.
  - The new version is enabled as live.

  Args:
    http_session: Authorized session for HTTP requests.
    rule_id: Unique ID of the detection rule to disable ("ru_<UUID>"). A version
      suffix should not be provided, because at most one version of a detection
      rule (by default the latest version of a rule) can be enabled at a time.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{rule_id}:disableLiveRule"

    response = http_session.request("POST", url)
    # Expected server response:
    # {}

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
def create_subject(http_session: requests.AuthorizedSession, name: str,
                   subject_type: str,
                   roles: Sequence[str]) -> Mapping[str, Sequence[Any]]:
    """Creates a subject.

  Args:
    http_session: Authorized session for HTTP requests.
    name: The ID of the subject to be created.
    subject_type: The type of the subject, e.g., SUBJECT_TYPE_ANALYST or
      SUBJECT_TYPE_IDP_GROUP.
    roles: The role(s) the created subject must have.

  Returns:
    Information about the newly created subject in the form:
    {
      "subject": {
        "name": "*****@*****.**",
        "type": "SUBJECT_TYPE_ANALYST",
        "roles": [
          {
            "name": "Test",
            "title": "Test role",
            "description": "The Test role",
            "createTime": "yyyy-mm-ddThh:mm:ss.ssssssZ",
            "isDefault": false,
            "permissions": [
              {
                "name": "Test",
                "title": "Test permission",
                "description": "The Test permission",
                "createTime": "yyyy-mm-ddThh:mm:ss.ssssssZ",
              },
              ...
            ]
          },
          ...
        ]
      }
    }

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v1/subjects/"

    body = {
        "name": name,
        "type": subject_type,
        "roles": [{
            "name": role
        } for role in roles],
    }
    response = http_session.request("POST", url, json=body)

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()
Exemple #26
0
def list_operations(http_session: requests.AuthorizedSession,
                    size_limit: int = 0) -> Sequence[get_operation.Operation]:
  """Retrieves details and states of all the operations of your organization.

  This API call does not support pagination at this time.

  Args:
    http_session: Authorized session for HTTP requests.
    size_limit: Maximum number of operations in the response. Must be
      non-negative. Optional - no client-side limit by default.

  Returns:
    All the detection operations of your organization.

  Raises:
    ValueError: Invalid input value.
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
  if size_limit < 0:
    raise ValueError(f"Invalid input: size_limit = {size_limit}, must be >= 0.")

  url = f"{CHRONICLE_API_BASE_URL}/v1/operations"
  if size_limit > 0:
    url += f"?page_size={size_limit}"

  response = http_session.request("GET", url)
  # Expected server response:
  # {
  #   "operations": [
  #     {
  #       "name": "operations/rulejob_jo_<UUID>",
  #       "metadata": {
  #         "@type": "...",
  #         "ruleId": "ru_<UUID>",
  #         "eventStartTime": "yyyy-mm-ddThh:mm:ssZ",
  #         "eventEndTime": "yyyy-mm-ddThh:mm:ssZ",
  #         "runStartedTime": "yyyy-mm-ddThh:mm:ssZ",
  #         "runCompletedTime": "yyyy-mm-ddThh:mm:ssZ"  <-- IFF "done" is true.
  #       },
  #       "done": true,  <-- If and only if operation's execution is finished.
  #       "error": {"code": <int>, "message": <str>}  <-- IFF there's an error.
  #       "response": {  <-- IFF "done" is true, and there is no error.
  #          "@type": "..."
  #       }
  #     },
  #     ...
  #   ]
  # }

  if response.status_code >= 400:
    print(response.text)
  response.raise_for_status()

  results = []
  for json in response.json()["operations"]:
    results.append(get_operation.Operation(json))
  return results
Exemple #27
0
def read_sheet(credentials_path, sheet_url, sheet_range, download_sheet=None):
    """
    Fetch Google Sheet values using specified range
    :param credentials_path: json file containing google credentials
    :param sheet_id: google sheet id
    :param sheet_range: range in Sheet format (example A1:E3)
    :return: array / sheet values
    """
    sheet_id = sheet_id_from_url(sheet_url)
    # Source: https://developers.google.com/sheets/api/quickstart/python
    creds = None
    SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']

    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.pickle'):
        logging.info('Existing Google session token found')
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            logging.info('Session expired. Restarting session on Google')
            creds.refresh(Request())
        else:
            logging.info('Getting authorization from Google')
            flow = InstalledAppFlow.from_client_secrets_file(
                credentials_path, SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            logging.info('Saving Google session token.')
            pickle.dump(creds, token)

    service = build('sheets', 'v4', credentials=creds, cache_discovery=False)
    # Call the Sheets API
    sheet = service.spreadsheets()
    logging.info('Fetching spreadsheet data')
    result = sheet.values().get(spreadsheetId=sheet_id,
                                range=sheet_range).execute()
    values = result.get('values', [])
    logging.debug(values)

    if download_sheet is not None:
        from google.auth.transport.requests import AuthorizedSession

        authed_session = AuthorizedSession(creds)
        exportUrl = sheet_url.replace('edit', 'export')

        url = exportUrl

        response = authed_session.request('GET', url)

        with open(download_sheet, 'wb') as csvFile:
            csvFile.write(response.content)

    return values
def list_asset_events(
    http_session: requests.AuthorizedSession,
    indicator: str,
    asset: str,
    start_time: datetime.datetime,
    end_time: datetime.datetime,
    ref_time: datetime.datetime,
    page_size: Optional[int] = 0
) -> Tuple[Sequence[Mapping[str, Any]], str, bool]:
    """Lists up to 10,000 UDM events that reference an asset in a time range.

  If there are more matching events than what was returned (according to the
  second element in the tuple returned by this function), you should split this
  call into multiple shorter time ranges to ensure you have visibility into all
  the available events.

  Args:
    http_session: Authorized session for HTTP requests.
    indicator: Type of asset indicator - either "hostname", "asset_ip_address",
      "mac", or "product_id".
    asset: Asset indicator value - a specific hostname, IP address, MAC address,
      or event ID from the logging product that generated the event.
    start_time: Inclusive beginning of the time range of events to return, with
      any timezone (even a timezone-unaware datetime object, i.e. local time).
    end_time: The exclusive end of the time range of events to return, with any
      timezone (even a timezone-unaware datetime object, i.e. local time).
    ref_time: Reference time, used to disambiguate asset indicators, which may
      refer to different assets at different points in time, with any timezone
      (even a timezone-unaware datetime object, i.e. local time).
    page_size: Maximum number of events to return, up to 10,000 (default =
      10,000).

  Returns:
    Tuple with 3 elements: (1) a list of all the UDM events (within the defined
    page) as Python dictionaries, (2) a Boolean value indicating whether there
    are matching events that were not returned, i.e. whether the matching event
    count exceeded the page size, and (3) a URL to see all these asset events in
    the Chronicle web UI.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
    (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v1/asset/listevents"
    params = {
        "asset." + indicator: asset,
        "start_time": datetime_converter.strftime(start_time),
        "end_time": datetime_converter.strftime(end_time),
        "reference_time": datetime_converter.strftime(ref_time),
        "page_size": page_size,
    }
    response = http_session.request("GET", url, params=params)

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    d = response.json()
    return d.get("events", []), d.get("moreDataAvailable", False), d["uri"][0]
def list_rules(http_session: requests.AuthorizedSession,
               page_size: int = 0,
               page_token: str = "") -> Tuple[Sequence[Mapping[str, Any]], str]:
  """List detection rules.

  Args:
    http_session: Authorized session for HTTP requests.
    page_size: Maximum number of rules to return.
      Must be non-negative, and is capped at a server-side limit of 1000.
      Optional - a server-side default of 100 is used if the size is 0 or a
      None value.
    page_token: Page token from a previous ListRules call used for pagination.
      Optional - the first page is retrieved if the token is the empty string
      or a None value.

  Returns:
    List of rules and a page token for the next page of rules, if there are
    any.

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
  url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules"
  params_list = [("page_size", page_size), ("page_token", page_token)]
  params = {k: v for k, v in params_list if v}

  response = http_session.request("GET", url, params=params)
  # Expected server response:
  # {
  #   "rules": [
  #     {
  #       "ruleId": "ru_<UUID>",
  #       "versionId": "ru_<UUID>@v_<seconds>_<nanoseconds>",
  #       "ruleName": "<rule_name>",
  #       "metadata": {
  #         "<key_1>": "<value_1>",
  #         "<key_2>": "<value_2>",
  #         ...
  #       },
  #       "ruleText": "<rule_content>",
  #       "alertingEnabled": true, <-- IFF alerting is enabled.
  #       "liveRuleEnabled": true, <-- IFF a live rule is enabled.
  #       "versionCreateTime": "yyyy-mm-ddThh:mm:ss.ssssssZ",
  #       "compilationState": "SUCCEEDED"/"FAILED",
  #       "compilationError": "<error_message>" <-- IFF compilation failed.
  #     },
  #     ...
  #   ],
  #   "nextPageToken": "<next_page_token>"
  # }

  if response.status_code >= 400:
    print(response.text)
  response.raise_for_status()
  json = response.json()
  return json.get("rules", []), json.get("nextPageToken", "")
Exemple #30
0
def update_subject(http_session: requests.AuthorizedSession, name: str,
                   roles: Sequence[str]) -> Mapping[str, Sequence[Any]]:
    """Updates information about a subject.

  Args:
    http_session: Authorized session for HTTP requests.
    name: The ID of the subject to retrieve information about.
    roles: The role(s) the subject must have after the update.

  Returns:
    Information about the newly updated subject in the form:
    {
      "subject": {
        "name": "*****@*****.**",
        "type": "SUBJECT_TYPE_ANALYST",
        "roles": [
          {
            "name": "Test",
            "title": "Test role",
            "description": "The Test role",
            "createTime": "yyyy-mm-ddThh:mm:ss.ssssssZ",
            "isDefault": false,
            "permissions": [
              {
                "name": "Test",
                "title": "Test permission",
                "description": "The Test permission",
                "createTime": "yyyy-mm-ddThh:mm:ss.ssssssZ",
              },
              ...
            ]
          },
          ...
        ]
      }
    }

  Raises:
    requests.exceptions.HTTPError: HTTP request resulted in an error
      (response.status_code >= 400).
  """
    url = f"{CHRONICLE_API_BASE_URL}/v1/subjects/{name}"
    body = {
        "name": name,
        "roles": [{
            "name": role
        } for role in roles],
    }
    update_fields = ["subject.roles"]
    params = {"update_mask": ",".join(update_fields)}
    response = http_session.request("PATCH", url, params=params, json=body)

    if response.status_code >= 400:
        print(response.text)
    response.raise_for_status()
    return response.json()