diff --git a/.github/.gitleaks.toml b/.github/.gitleaks.toml new file mode 100644 index 000000000..3aba803a3 --- /dev/null +++ b/.github/.gitleaks.toml @@ -0,0 +1,538 @@ +title = "gitleaks config" + +# Gitleaks rules are defined by regular expressions and entropy ranges. +# Some secrets have unique signatures which make detecting those secrets easy. +# Examples of those secrets would be GitLab Personal Access Tokens, AWS keys, and GitHub Access Tokens. +# All these examples have defined prefixes like `glpat`, `AKIA`, `ghp_`, etc. +# +# Other secrets might just be a hash which means we need to write more complex rules to verify +# that what we are matching is a secret. +# +# Here is an example of a semi-generic secret +# +# discord_client_secret = "8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ" +# +# We can write a regular expression to capture the variable name (identifier), +# the assignment symbol (like '=' or ':='), and finally the actual secret. +# The structure of a rule to match this example secret is below: +# +# Beginning string +# quotation +# │ End string quotation +# │ │ +# ▼ ▼ +# (?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"] +# +# ▲ ▲ ▲ +# │ │ │ +# │ │ │ +# identifier assignment symbol +# Secret +# +[[rules]] +id = "gitlab-pat" +description = "GitLab Personal Access Token" +regex = '''glpat-[0-9a-zA-Z\-\_]{20}''' + +[[rules]] +id = "aws-access-token" +description = "AWS" +regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' + +# Cryptographic keys +[[rules]] +id = "PKCS8-PK" +description = "PKCS8 private key" +regex = '''-----BEGIN PRIVATE KEY-----''' + +[[rules]] +id = "RSA-PK" +description = "RSA private key" +regex = '''-----BEGIN RSA PRIVATE KEY-----''' + +[[rules]] +id = "OPENSSH-PK" +description = "SSH private key" +regex = '''-----BEGIN OPENSSH PRIVATE KEY-----''' + +[[rules]] +id = "PGP-PK" +description = "PGP private key" +regex = '''-----BEGIN PGP PRIVATE KEY BLOCK-----''' + +[[rules]] +id = "github-pat" +description = "GitHub Personal Access Token" +regex = '''ghp_[0-9a-zA-Z]{36}''' + +[[rules]] +id = "github-oauth" +description = "GitHub OAuth Access Token" +regex = '''gho_[0-9a-zA-Z]{36}''' + +[[rules]] +id = "SSH-DSA-PK" +description = "SSH (DSA) private key" +regex = '''-----BEGIN DSA PRIVATE KEY-----''' + +[[rules]] +id = "SSH-EC-PK" +description = "SSH (EC) private key" +regex = '''-----BEGIN EC PRIVATE KEY-----''' + + +[[rules]] +id = "github-app-token" +description = "GitHub App Token" +regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}''' + +[[rules]] +id = "github-refresh-token" +description = "GitHub Refresh Token" +regex = '''ghr_[0-9a-zA-Z]{76}''' + +[[rules]] +id = "shopify-shared-secret" +description = "Shopify shared secret" +regex = '''shpss_[a-fA-F0-9]{32}''' + +[[rules]] +id = "shopify-access-token" +description = "Shopify access token" +regex = '''shpat_[a-fA-F0-9]{32}''' + +[[rules]] +id = "shopify-custom-access-token" +description = "Shopify custom app access token" +regex = '''shpca_[a-fA-F0-9]{32}''' + +[[rules]] +id = "shopify-private-app-access-token" +description = "Shopify private app access token" +regex = '''shppa_[a-fA-F0-9]{32}''' + +[[rules]] +id = "slack-access-token" +description = "Slack token" +regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?''' + +[[rules]] +id = "stripe-access-token" +description = "Stripe" +regex = '''(?i)(sk|pk)_(test|live)_[0-9a-z]{10,32}''' + +[[rules]] +id = "pypi-upload-token" +description = "PyPI upload token" +regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}''' + +[[rules]] +id = "gcp-service-account" +description = "Google (GCP) Service-account" +regex = '''\"type\": \"service_account\"''' + +[[rules]] +id = "heroku-api-key" +description = "Heroku API Key" +regex = ''' (?i)(heroku[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})['\"]''' +secretGroup = 3 + +[[rules]] +id = "slack-web-hook" +description = "Slack Webhook" +regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}''' + +[[rules]] +id = "twilio-api-key" +description = "Twilio API Key" +regex = '''SK[0-9a-fA-F]{32}''' + +[[rules]] +id = "age-secret-key" +description = "Age secret key" +regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}''' + +[[rules]] +id = "facebook-token" +description = "Facebook token" +regex = '''(?i)(facebook[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "twitter-token" +description = "Twitter token" +regex = '''(?i)(twitter[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{35,44})['\"]''' +secretGroup = 3 + +[[rules]] +id = "adobe-client-id" +description = "Adobe Client ID (Oauth Web)" +regex = '''(?i)(adobe[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "adobe-client-secret" +description = "Adobe Client Secret" +regex = '''(p8e-)(?i)[a-z0-9]{32}''' + +[[rules]] +id = "alibaba-access-key-id" +description = "Alibaba AccessKey ID" +regex = '''(LTAI)(?i)[a-z0-9]{20}''' + +[[rules]] +id = "alibaba-secret-key" +description = "Alibaba Secret Key" +regex = '''(?i)(alibaba[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]''' +secretGroup = 3 + +[[rules]] +id = "asana-client-id" +description = "Asana Client ID" +regex = '''(?i)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{16})['\"]''' +secretGroup = 3 + +[[rules]] +id = "asana-client-secret" +description = "Asana Client Secret" +regex = '''(?i)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "atlassian-api-token" +description = "Atlassian API token" +regex = '''(?i)(atlassian[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{24})['\"]''' +secretGroup = 3 + +[[rules]] +id = "bitbucket-client-id" +description = "Bitbucket client ID" +regex = '''(?i)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "bitbucket-client-secret" +description = "Bitbucket client secret" +regex = '''(?i)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9_\-]{64})['\"]''' +secretGroup = 3 + +[[rules]] +id = "beamer-api-token" +description = "Beamer API token" +regex = '''(?i)(beamer[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](b_[a-z0-9=_\-]{44})['\"]''' +secretGroup = 3 + +[[rules]] +id = "clojars-api-token" +description = "Clojars API token" +regex = '''(CLOJARS_)(?i)[a-z0-9]{60}''' + +[[rules]] +id = "contentful-delivery-api-token" +description = "Contentful delivery API token" +regex = '''(?i)(contentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{43})['\"]''' +secretGroup = 3 + +[[rules]] +id = "databricks-api-token" +description = "Databricks API token" +regex = '''dapi[a-h0-9]{32}''' + +[[rules]] +id = "discord-api-token" +description = "Discord API key" +regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]''' +secretGroup = 3 + +[[rules]] +id = "discord-client-id" +description = "Discord client ID" +regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{18})['\"]''' +secretGroup = 3 + +[[rules]] +id = "discord-client-secret" +description = "Discord client secret" +regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "doppler-api-token" +description = "Doppler API token" +regex = '''['\"](dp\.pt\.)(?i)[a-z0-9]{43}['\"]''' + +[[rules]] +id = "dropbox-api-secret" +description = "Dropbox API secret/key" +regex = '''(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]''' + +[[rules]] +id = "dropbox--api-key" +description = "Dropbox API secret/key" +regex = '''(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]''' + +[[rules]] +id = "dropbox-short-lived-api-token" +description = "Dropbox short lived API token" +regex = '''(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](sl\.[a-z0-9\-=_]{135})['\"]''' + +[[rules]] +id = "dropbox-long-lived-api-token" +description = "Dropbox long lived API token" +regex = '''(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"][a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43}['\"]''' + +[[rules]] +id = "duffel-api-token" +description = "Duffel API token" +regex = '''['\"]duffel_(test|live)_(?i)[a-z0-9_-]{43}['\"]''' + +[[rules]] +id = "dynatrace-api-token" +description = "Dynatrace API token" +regex = '''['\"]dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}['\"]''' + +[[rules]] +id = "easypost-api-token" +description = "EasyPost API token" +regex = '''['\"]EZAK(?i)[a-z0-9]{54}['\"]''' + +[[rules]] +id = "easypost-test-api-token" +description = "EasyPost test API token" +regex = '''['\"]EZTK(?i)[a-z0-9]{54}['\"]''' + +[[rules]] +id = "fastly-api-token" +description = "Fastly API token" +regex = '''(?i)(fastly[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "finicity-client-secret" +description = "Finicity client secret" +regex = '''(?i)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{20})['\"]''' +secretGroup = 3 + +[[rules]] +id = "finicity-api-token" +description = "Finicity API token" +regex = '''(?i)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "flutterwave-public-key" +description = "Flutterwave public key" +regex = '''FLWPUBK_TEST-(?i)[a-h0-9]{32}-X''' + +[[rules]] +id = "flutterwave-secret-key" +description = "Flutterwave secret key" +regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X''' + +[[rules]] +id = "flutterwave-enc-key" +description = "Flutterwave encrypted key" +regex = '''FLWSECK_TEST[a-h0-9]{12}''' + +[[rules]] +id = "frameio-api-token" +description = "Frame.io API token" +regex = '''fio-u-(?i)[a-z0-9\-_=]{64}''' + +[[rules]] +id = "gocardless-api-token" +description = "GoCardless API token" +regex = '''['\"]live_(?i)[a-z0-9\-_=]{40}['\"]''' + +[[rules]] +id = "grafana-api-token" +description = "Grafana API token" +regex = '''['\"]eyJrIjoi(?i)[a-z0-9\-_=]{72,92}['\"]''' + +[[rules]] +id = "hashicorp-tf-api-token" +description = "HashiCorp Terraform user/org API token" +regex = '''['\"](?i)[a-z0-9]{14}\.atlasv1\.[a-z0-9\-_=]{60,70}['\"]''' + +[[rules]] +id = "hubspot-api-token" +description = "HubSpot API token" +regex = '''(?i)(hubspot[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]''' +secretGroup = 3 + +[[rules]] +id = "intercom-api-token" +description = "Intercom API token" +regex = '''(?i)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_]{60})['\"]''' +secretGroup = 3 + +[[rules]] +id = "intercom-client-secret" +description = "Intercom client secret/ID" +regex = '''(?i)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]''' +secretGroup = 3 + +[[rules]] +id = "ionic-api-token" +description = "Ionic API token" +regex = '''(?i)(ionic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](ion_[a-z0-9]{42})['\"]''' + +[[rules]] +id = "linear-api-token" +description = "Linear API token" +regex = '''lin_api_(?i)[a-z0-9]{40}''' + +[[rules]] +id = "linear-client-secret" +description = "Linear client secret/ID" +regex = '''(?i)(linear[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "lob-api-key" +description = "Lob API Key" +regex = '''(?i)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((live|test)_[a-f0-9]{35})['\"]''' +secretGroup = 3 + +[[rules]] +id = "lob-pub-api-key" +description = "Lob Publishable API Key" +regex = '''(?i)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((test|live)_pub_[a-f0-9]{31})['\"]''' +secretGroup = 3 + +[[rules]] +id = "mailchimp-api-key" +description = "Mailchimp API key" +regex = '''(?i)(mailchimp[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32}-us20)['\"]''' +secretGroup = 3 + +[[rules]] +id = "mailgun-private-api-token" +description = "Mailgun private API token" +regex = '''(?i)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](key-[a-f0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "mailgun-pub-key" +description = "Mailgun public validation key" +regex = '''(?i)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](pubkey-[a-f0-9]{32})['\"]''' +secretGroup = 3 + +[[rules]] +id = "mailgun-signing-key" +description = "Mailgun webhook signing key" +regex = '''(?i)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})['\"]''' +secretGroup = 3 + +[[rules]] +id = "mapbox-api-token" +description = "Mapbox API token" +regex = '''(?i)(pk\.[a-z0-9]{60}\.[a-z0-9]{22})''' + +[[rules]] +id = "messagebird-api-token" +description = "MessageBird API token" +regex = '''(?i)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{25})['\"]''' +secretGroup = 3 + +[[rules]] +id = "messagebird-client-id" +description = "MessageBird API client ID" +regex = '''(?i)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]''' +secretGroup = 3 + +[[rules]] +id = "new-relic-user-api-key" +description = "New Relic user API Key" +regex = '''['\"](NRAK-[A-Z0-9]{27})['\"]''' + +[[rules]] +id = "new-relic-user-api-id" +description = "New Relic user API ID" +regex = '''(?i)(newrelic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([A-Z0-9]{64})['\"]''' +secretGroup = 3 + +[[rules]] +id = "new-relic-browser-api-token" +description = "New Relic ingest browser API token" +regex = '''['\"](NRJS-[a-f0-9]{19})['\"]''' + +[[rules]] +id = "npm-access-token" +description = "npm access token" +regex = '''['\"](npm_(?i)[a-z0-9]{36})['\"]''' + +[[rules]] +id = "planetscale-password" +description = "PlanetScale password" +regex = '''pscale_pw_(?i)[a-z0-9\-_\.]{43}''' + +[[rules]] +id = "planetscale-api-token" +description = "PlanetScale API token" +regex = '''pscale_tkn_(?i)[a-z0-9\-_\.]{43}''' + +[[rules]] +id = "postman-api-token" +description = "Postman API token" +regex = '''PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34}''' + +[[rules]] +id = "pulumi-api-token" +description = "Pulumi API token" +regex = '''pul-[a-f0-9]{40}''' + +[[rules]] +id = "rubygems-api-token" +description = "Rubygem API token" +regex = '''rubygems_[a-f0-9]{48}''' + +[[rules]] +id = "sendgrid-api-token" +description = "SendGrid API token" +regex = '''SG\.(?i)[a-z0-9_\-\.]{66}''' + +[[rules]] +id = "sendinblue-api-token" +description = "Sendinblue API token" +regex = '''xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16}''' + +[[rules]] +id = "shippo-api-token" +description = "Shippo API token" +regex = '''shippo_(live|test)_[a-f0-9]{40}''' + +[[rules]] +id = "linkedin-client-secret" +description = "LinkedIn Client secret" +regex = '''(?i)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z]{16})['\"]''' +secretGroup = 3 + +[[rules]] +id = "linkedin-client-id" +description = "LinkedIn Client ID" +regex = '''(?i)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{14})['\"]''' +secretGroup = 3 + +[[rules]] +id = "twitch-api-token" +description = "Twitch API token" +regex = '''(?i)(twitch[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]''' +secretGroup = 3 + +[[rules]] +id = "typeform-api-token" +description = "Typeform API token" +regex = '''(?i)(typeform[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}(tfp_[a-z0-9\-_\.=]{59})''' +secretGroup = 3 + + +[allowlist] +description = "global allow lists" +regexes = ['''219-09-9999''', '''078-05-1120''', '''(9[0-9]{2}|666)-\d{2}-\d{4}''', '''RPM-GPG-KEY.*''', '''.*:.*StrelkaHexDump.*''', '''.*:.*PLACEHOLDER.*'''] +paths = [ + '''gitleaks.toml''', + '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''', + '''(go.mod|go.sum)$''', + '''salt/nginx/files/enterprise-attack.json''' +] diff --git a/.github/workflows/leaktest.yml b/.github/workflows/leaktest.yml index cc6d913d0..687f7b554 100644 --- a/.github/workflows/leaktest.yml +++ b/.github/workflows/leaktest.yml @@ -13,3 +13,5 @@ jobs: - name: Gitleaks uses: gitleaks/gitleaks-action@v1.6.0 + with: + config-path: .github/.gitleaks.toml diff --git a/Dockerfile b/Dockerfile index fe85fc588..8a95a7011 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,14 @@ # https://securityonion.net/license; you may not use this file except in compliance with the # Elastic License 2.0. -FROM ghcr.io/security-onion-solutions/golang:1.21.5-alpine as builder +FROM ghcr.io/security-onion-solutions/golang:1.22-alpine as builder ARG VERSION=0.0.0 RUN apk update && apk add libpcap-dev bash git musl-dev gcc npm python3 py3-pip py3-virtualenv python3-dev openssl-dev linux-headers COPY . /build WORKDIR /build RUN if [ "$VERSION" != "0.0.0" ]; then mkdir gitdocs && cd gitdocs && \ git clone --no-single-branch --depth 50 https://github.com/Security-Onion-Solutions/securityonion-docs.git . && \ - git checkout --force origin/$(echo $VERSION | cut -d'.' -f1,2) && \ + git checkout --force origin/dev && \ git clean -d -f -f && \ sed -i "s|'display_github': True|'display_github': False|g" conf.py && \ python3 -mvirtualenv /tmp/virtualenv && \ @@ -22,9 +22,14 @@ RUN if [ "$VERSION" != "0.0.0" ]; then mkdir gitdocs && cd gitdocs && \ RUN npm install jest jest-environment-jsdom --global RUN ./build.sh "$VERSION" -RUN pip3 install sigma-cli pysigma-backend-elasticsearch pysigma-pipeline-windows yara-python --break-system-packages +RUN pip3 install sigma-cli pysigma-backend-elasticsearch pysigma-pipeline-windows --break-system-packages RUN sed -i 's/#!\/usr\/bin\/python3/#!\/usr\/bin\/env python/g' /usr/bin/sigma +# Build specific version of yara-python - needs to be pinned to Strelka's version. +FROM ghcr.io/security-onion-solutions/python:3-slim as stage_2 +RUN apt-get update && apt-get install -y gcc python3-dev libssl-dev +RUN pip3 install yara-python==4.3.1 + FROM ghcr.io/security-onion-solutions/python:3-slim ARG UID=939 @@ -34,7 +39,7 @@ ARG ELASTIC_VERSION=0.0.0 ARG WAZUH_VERSION=0.0.0 RUN apt update -y -RUN apt install -y bash tzdata ca-certificates wget curl tcpdump unzip +RUN apt install -y bash tzdata ca-certificates wget curl tcpdump unzip git RUN update-ca-certificates RUN addgroup --gid "$GID" socore RUN adduser --disabled-password --uid "$UID" --ingroup socore --gecos '' socore @@ -51,6 +56,8 @@ COPY --from=builder /build/sensoroni.json . COPY --from=builder /build/gitdocs/_build/html ./html/docs COPY --from=builder /usr/lib/python3.11/site-packages /usr/local/lib/python3.9/site-packages COPY --from=builder /usr/bin/sigma /usr/bin/sigma +COPY --from=stage_2 /usr/local/lib/python3.9/site-packages/yara_python-4.3.1.dist-info /usr/local/lib/python3.9/site-packages/ +COPY --from=stage_2 /usr/local/lib/python3.9/site-packages/yara.cpython-39-x86_64-linux-gnu.so /usr/local/lib/python3.9/site-packages/ RUN find html/js -name "*test*.js" -delete RUN chmod u+x scripts/* RUN chown 939:939 scripts/* diff --git a/Dockerfile.kratos b/Dockerfile.kratos index 4a05b55fe..c69aa5ca9 100644 --- a/Dockerfile.kratos +++ b/Dockerfile.kratos @@ -4,7 +4,7 @@ # https://securityonion.net/license; you may not use this file except in compliance with the # Elastic License 2.0. -FROM ghcr.io/security-onion-solutions/golang:1.21 AS builder +FROM ghcr.io/security-onion-solutions/golang:1.22 AS builder ARG OWNER=ory ARG VERSION=v1.1.0 @@ -30,7 +30,7 @@ ENV CGO_CPPFLAGS -DSQLITE_DEFAULT_FILE_PERMISSIONS=0600 RUN go mod download RUN go build -tags sqlite -ldflags="-X 'github.com/ory/kratos/driver/config.Version=${VERSION}' -X 'github.com/ory/kratos/driver/config.Date=$(date -I)' -X 'github.com/ory/kratos/driver/config.Commit=$(git rev-parse --short HEAD)'" - + FROM ghcr.io/security-onion-solutions/ubuntu:23.04 diff --git a/config/clientparameters.go b/config/clientparameters.go index 119bfadc0..7856e3ea3 100644 --- a/config/clientparameters.go +++ b/config/clientparameters.go @@ -22,9 +22,8 @@ type ClientParameters struct { CaseParams CaseParameters `json:"case"` DashboardsParams HuntingParameters `json:"dashboards"` JobParams HuntingParameters `json:"job"` - DetectionsParams HuntingParameters `json:"detections"` + DetectionsParams DetectionParameters `json:"detections"` DetectionParams DetectionParameters `json:"detection"` - PlaybooksParams HuntingParameters `json:"playbooks"` DocsUrl string `json:"docsUrl"` CheatsheetUrl string `json:"cheatsheetUrl"` ReleaseNotesUrl string `json:"releaseNotesUrl"` diff --git a/go.mod b/go.mod index 8576f0698..9406e4c60 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/security-onion-solutions/securityonion-soc -go 1.21 +go 1.22 require ( github.com/apex/log v1.9.0 @@ -13,14 +13,15 @@ require ( github.com/kennygrant/sanitize v1.2.4 github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.17.0 - golang.org/x/crypto v0.17.0 - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/crypto v0.21.0 + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/go-git/go-git/v5 v5.11.0 + github.com/hashicorp/go-multierror v1.1.1 github.com/pierrec/lz4/v4 v4.1.21 github.com/pkg/errors v0.9.1 github.com/samber/lo v1.39.0 @@ -41,11 +42,11 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/oapi-codegen/runtime v1.0.0 // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect diff --git a/go.sum b/go.sum index 8bed13150..bb7cb3887 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,10 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= @@ -98,8 +102,6 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -157,8 +159,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -176,8 +178,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -200,15 +202,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/html/css/app.css b/html/css/app.css index a3cefced7..ac039959a 100644 --- a/html/css/app.css +++ b/html/css/app.css @@ -534,17 +534,6 @@ td { margin-top: 20px; } -.summary-backdrop { - background-color: rgb(53, 53, 53); - border-radius: 4px; -} - -.summary-backdrop > div { - background-color: #2a2a2a; - border-radius: 4px; - padding: 16px; -} - .header { font-size: 1.3em; font-weight: bold; @@ -555,7 +544,6 @@ td { } .extracted-content { - background-color: #404040; border-radius: 4px; padding: 8px; } @@ -565,32 +553,15 @@ td { word-break: break-word; } -.detect-reference { - font-size: small; - display: table; - margin-top: 12px; - margin-left: 12px; -} - -.detect-reference > div { - display: table-row; -} - -.detect-reference > div > div { - display: table-cell; - padding: 0 4px; -} - -.detect-reference .key { - font-weight: bold; - text-align: right; -} - .ops-header { font-weight: bold; margin-top: 16px; } +.ops-value::after { + content: "\00a0"; +} + .ops-value > .v-input--selection-controls { margin-top: 0 !important; padding-top: 0 !important; @@ -610,12 +581,6 @@ td { text-transform: capitalize; } -.sigma-dialog-body { - background-color: #404040; - margin: 12px; - padding: 12px; -} - #convert-button-container { display: flex; flex-direction: row; @@ -630,4 +595,37 @@ td { #actionTuneDetection-disabled { color: gray; cursor: pointer; +} + +.override-help { + vertical-align: -webkit-baseline-middle; + margin-right: 8px; +} + +.manual-sync { + margin: 6px; + display: flex; +} + +.manual-sync div:first-child { + flex-grow: 1; + margin-right: 8px; +} + +.manual-sync-buttons { + display: flex; + flex-direction: column; +} + +.manual-sync-buttons button:first-child { + margin-bottom: 6px; +} + +.manual-sync-buttons button { + width: 100%; +} + +#override-filter-edit-buttons { + display: flex; + justify-content: end; } \ No newline at end of file diff --git a/html/index.html b/html/index.html index a065fb89d..a79737b4f 100644 --- a/html/index.html +++ b/html/index.html @@ -20,12 +20,13 @@ Security Onion + - - - diff --git a/html/js/analytics.js b/html/js/analytics.js new file mode 100644 index 000000000..e69de29bb diff --git a/html/js/app.js b/html/js/app.js index ff303481a..b9503bc06 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -21,6 +21,8 @@ const USER_PASSWORD_LENGTH_MIN = 8; const USER_PASSWORD_LENGTH_MAX = 72; const USER_PASSWORD_INVALID_RX = /["'$&!]/; +const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; + if (typeof global !== 'undefined') global.routes = routes; $(document).ready(function() { @@ -379,10 +381,25 @@ $(document).ready(function() { if (u.host.toUpperCase() == window.location.host.toUpperCase()) { url = u.hash; } - const content = this.i18n.gridMemberImportSuccess.replace('<[url]>', url); + const content = this.i18n.gridMemberImportSuccess.replace('{url}', url); this.showInfo(content); } }); + this.subscribe('detection-sync', (report) => { + const eng = this.correctCasing(report.engine); + + switch (report.status) { + case 'success': + this.showInfo(this.i18n.syncSuccess.replace('{engine}', eng)); + break; + case 'partial': + this.showWarning(this.i18n.syncPartialSuccess.replace('{engine}', eng)); + break; + case 'error': + this.showError(this.i18n.syncFailure.replace('{engine}', eng)); + break; + } + }); } } catch (error) { if (!background) { @@ -614,6 +631,23 @@ $(document).ready(function() { } return localized; }, + tryLocalize(msg) { + const localized = this.localizeMessage(msg); + if (localized) { + return localized; + } + + return msg; + }, + correctCasing(origMsg) { + const msg = (origMsg+'').toLowerCase(); + var localized = this.i18n['cc_'+msg]; + if (!localized) { + return origMsg; + } + + return localized; + }, showError(msg) { this.error = true; this.errorMessage = this.localizeMessage(msg); @@ -898,7 +932,13 @@ $(document).ready(function() { }, async populateUserDetails(obj, idField, outputField) { if (obj[idField] && obj[idField].length > 0) { - const user = await this.$root.getUserById(obj[idField]); + const id = obj[idField]; + if (id === SYSTEM_USER_ID || id === "agent") { + Vue.set(obj, outputField, this.i18n.systemUser); + return + } + + const user = await this.$root.getUserById(id); if (user) { Vue.set(obj, outputField, user.email); } @@ -927,6 +967,70 @@ $(document).ready(function() { this.updateTitle(); this.loadServerSettings(true); }, + getDetectionEngines() { + return ['elastalert', 'strelka', 'suricata']; + }, + getDetectionEngineStatusClass(engine) { + switch (this.getDetectionEngineStatus(engine)) { + case "MigrationFailure": return "warning--text"; + case "SyncFailure": return "warning--text"; + case "IntegrityFailure": return "warning--text"; + case "Healthy": return "success--text"; + } + return "normal--text"; + }, + getDetectionEngineStatus(engine) { + if (!this.currentStatus || !this.currentStatus.detections || !this.currentStatus.detections[engine]) { + return "Unknown"; + } + + const status = this.currentStatus.detections[engine]; + + // Order is important in this if/else block. Certain status should take priority. For example, + // If a sync failure and integrity failure both occurred then show the integrity failure, because + // if it got to the integrity check then the sync finished but the integrity check failed. + if (status.migrating) { + return "Migrating"; + } else if (status.importing && status.syncing) { + return "Importing"; + } else if (status.migrationFailure) { + return "MigrationFailure"; + } else if (status.integrityFailure) { + return "IntegrityFailure"; + } else if (status.syncFailure) { + return "SyncFailure"; + } else if (status.importing && !status.syncing) { + return "ImportPending"; + } else if (status.syncing) { + return "Syncing"; + } + return "Healthy"; + }, + isDetectionsUnhealthy() { + return this.currentStatus != null && this.currentStatus.detections != null && + ( this.currentStatus.detections.elastalert.integrityFailure || + this.currentStatus.detections.suricata.integrityFailure || + this.currentStatus.detections.strelka.integrityFailure || + this.currentStatus.detections.elastalert.syncFailure || + this.currentStatus.detections.suricata.syncFailure || + this.currentStatus.detections.strelka.syncFailure || + this.currentStatus.detections.elastalert.migrationFailure || + this.currentStatus.detections.suricata.migrationFailure || + this.currentStatus.detections.strelka.migrationFailure ); + }, + isDetectionsUpdating() { + return this.currentStatus != null && this.currentStatus.detections != null && + !this.isDetectionsUnhealthy() && + ( this.currentStatus.detections.elastalert.importing === true || + this.currentStatus.detections.elastalert.migrating === true || + this.currentStatus.detections.elastalert.syncing === true || + this.currentStatus.detections.strelka.importing === true || + this.currentStatus.detections.strelka.migrating === true || + this.currentStatus.detections.strelka.syncing === true || + this.currentStatus.detections.suricata.importing === true || + this.currentStatus.detections.suricata.migrating === true || + this.currentStatus.detections.suricata.syncing === true ); + }, isGridUnhealthy() { return this.currentStatus && this.currentStatus.grid.unhealthyNodeCount > 0 }, @@ -934,7 +1038,7 @@ $(document).ready(function() { return this.currentStatus && this.currentStatus.alerts.newCount > 0 }, isAttentionNeeded() { - return this.isNewAlert() || this.isGridUnhealthy() || !this.connected || this.reconnecting; + return this.isNewAlert() || this.isGridUnhealthy() || this.isDetectionsUnhealthy() || !this.connected || this.reconnecting; }, isMaximized() { return this.maximizedTarget != null; diff --git a/html/js/app.test.js b/html/js/app.test.js index 8669c6636..ad87e4c1c 100644 --- a/html/js/app.test.js +++ b/html/js/app.test.js @@ -4,6 +4,7 @@ // https://securityonion.net/license; you may not use this file except in compliance with the // Elastic License 2.0. +require('./test_common.js'); require('./test_common.js'); const app = global.getApp(); @@ -125,6 +126,22 @@ test('populateUserDetails', async () => { expect(obj.owner).toBe('hi@there.net'); }); +test('populateUserDetailsSystem', async () => { + const obj = {userId:'00000000-0000-0000-0000-000000000000'}; + app.users = [{id:'123',email:'hi@there.net'}]; + app.usersLoadedTime = new Date().time; + await app.populateUserDetails(obj, "userId", "owner") + expect(obj.owner).toBe(app.i18n.systemUser); +}); + +test('populateUserDetailsAgent', async () => { + const obj = {userId:'agent'}; + app.users = [{id:'123',email:'hi@there.net'}]; + app.usersLoadedTime = new Date().time; + await app.populateUserDetails(obj, "userId", "owner") + expect(obj.owner).toBe(app.i18n.systemUser); +}); + test('isUserAdmin', async () => { var user = {id:'123',email:'hi@there.net',roles:['nope', 'peon']}; app.user = user; @@ -469,3 +486,225 @@ test('checkForUnauthorized', () => { testCheckForUnauthorized('/login/banner.md', {}, '/blah', false); testCheckForUnauthorized('/auth/self-service/login/browser', {}, '/blah', true); }); + +test('correctCasing', () => { + expect(app.correctCasing('')).toBe(''); + expect(app.correctCasing('foo')).toBe('foo'); + expect(app.correctCasing('FOO')).toBe('FOO'); + expect(app.correctCasing('yara')).toBe('YARA'); + expect(app.correctCasing('Yara')).toBe('YARA'); + expect(app.correctCasing('yArA')).toBe('YARA'); +}); + +function verifyEngineFailureStates(e1f1, e1f2, e1f3, e2f1, e2f2, e2f3, e3f1, e3f2, e3f3, expected) { + app.currentStatus = { detections: { + elastalert: { + integrityFailure: e1f1, + syncFailure: e1f2, + migrationFailure: e1f3, + }, + strelka: { + integrityFailure: e2f1, + syncFailure: e2f2, + migrationFailure: e2f3, + }, + suricata: { + integrityFailure: e3f1, + syncFailure: e3f2, + migrationFailure: e3f3, + }, + }} + expect(app.isDetectionsUnhealthy()).toBe(expected); +} + +test('isDetectionsUnhealthy', () => { + // Unhealthy + verifyEngineFailureStates(true, false, false, true, false, false, true, false, false, true); + verifyEngineFailureStates(false, true, false, false, true, false, false, true, false, true); + verifyEngineFailureStates(false, false, true, false, false, true, false, false, true, true); + verifyEngineFailureStates(true, true, false, true, true, false, true, true, false, true); + verifyEngineFailureStates(false, true, true, false, true, true, false, true, true, true); + verifyEngineFailureStates(true, false, true, true, false, true, true, false, true, true); + verifyEngineFailureStates(true, true, true, true, true, true, true, true, true, true); + verifyEngineFailureStates(true, true, true, true, true, true, true, true, true, true); + verifyEngineFailureStates(false, false, true, true, true, true, true, true, true, true); + verifyEngineFailureStates(false, false, false, true, true, true, true, true, true, true); + verifyEngineFailureStates(false, false, false, false, true, true, true, true, true, true); + verifyEngineFailureStates(false, false, false, false, false, true, true, true, true, true); + verifyEngineFailureStates(false, false, false, false, false, false, true, true, true, true); + verifyEngineFailureStates(false, false, false, false, false, false, false, true, true, true); + verifyEngineFailureStates(false, false, false, false, false, false, false, false, true, true); + + // Healthy + verifyEngineFailureStates(false, false, false, false, false, false, false, false, false, false); + + // Neither Unhealthy nor Healthy + app.currentStatus.detections.elastalert.migrating = true + app.currentStatus.detections.strelka.importing = true + app.currentStatus.detections.suricata.syncing = true + expect(app.isDetectionsUnhealthy()).toBe(false); +}); + +test('isDetectionsUpdating', () => { + // Unhealthy + app.currentStatus = { detections: { + elastalert: { + integrityFailure: true, + }, + strelka: { + integrityFailure: true, + }, + suricata: { + integrityFailure: true, + }, + }}; + expect(app.isDetectionsUpdating()).toBe(false); + + // All healthy + app.currentStatus.detections.elastalert.integrityFailure = false; + expect(app.isDetectionsUpdating()).toBe(false); + app.currentStatus.detections.strelka.integrityFailure = false; + expect(app.isDetectionsUpdating()).toBe(false); + app.currentStatus.detections.suricata.integrityFailure = false; + expect(app.isDetectionsUpdating()).toBe(false); + + // Suricata migrating + app.currentStatus.detections.suricata.migrating = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.suricata.migrating = false; + + // Strelka migrating + app.currentStatus.detections.strelka.migrating = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.strelka.migrating = false; + + // ElastAlert migrating + app.currentStatus.detections.elastalert.migrating = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.elastalert.migrating = false; + + // Suricata importing + app.currentStatus.detections.suricata.importing = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.suricata.importing = false; + + // Strelka importing + app.currentStatus.detections.strelka.importing = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.strelka.importing = false; + + // ElastAlert importing + app.currentStatus.detections.elastalert.importing = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.elastalert.importing = false; + + // Suricata syncing + app.currentStatus.detections.suricata.syncing = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.suricata.syncing = false; + + // Strelka syncing + app.currentStatus.detections.strelka.syncing = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.strelka.syncing = false; + + // ElastAlert syncing + app.currentStatus.detections.elastalert.syncing = true; + expect(app.isDetectionsUpdating()).toBe(true); + app.currentStatus.detections.elastalert.syncing = false; +}); + +test('getDetectionEngines', () => { + expect(app.getDetectionEngines()).toStrictEqual(['elastalert', 'strelka', 'suricata']); +}); + +test('getDetectionEngineStatusClass', () => { + expect(app.getDetectionEngineStatusClass('unknown')).toBe('normal--text'); + app.currentStatus = { detections: { strelka: { syncing: true }}}; + expect(app.getDetectionEngineStatusClass('strelka')).toBe('normal--text'); + app.currentStatus = { detections: { strelka: { migrationFailure: true, syncFailure: true }}}; + expect(app.getDetectionEngineStatusClass('strelka')).toBe('warning--text'); + app.currentStatus = { detections: { strelka: { syncFailure: true, integrityFailure: true }}}; + expect(app.getDetectionEngineStatusClass('strelka')).toBe('warning--text'); + app.currentStatus = { detections: { strelka: { integrityFailure: true, syncing: true }}}; + expect(app.getDetectionEngineStatusClass('strelka')).toBe('warning--text'); + app.currentStatus = { detections: { strelka: { migrating: true, integrityFailure: true }}}; + expect(app.getDetectionEngineStatusClass('strelka')).toBe('normal--text'); + app.currentStatus = { detections: { strelka: { importing: true, migrating: true }}}; + expect(app.getDetectionEngineStatusClass('strelka')).toBe('normal--text'); + app.currentStatus = { detections: { strelka: { importing: false }}}; + expect(app.getDetectionEngineStatusClass('strelka')).toBe('success--text'); +}); + +test('getDetectionEngineStatus', () => { + expect(app.getDetectionEngineStatus('unknown')).toBe('Unknown'); + app.currentStatus = { detections: { strelka: { syncing: true }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('Syncing'); + app.currentStatus = { detections: { strelka: { migrationFailure: true, syncFailure: true }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('MigrationFailure'); + app.currentStatus = { detections: { strelka: { syncFailure: true, integrityFailure: true }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('IntegrityFailure'); + app.currentStatus = { detections: { strelka: { syncFailure: true, integrityFailure: false }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('SyncFailure'); + app.currentStatus = { detections: { strelka: { migrating: true, importing: true, syncing: true, integrityFailure: true }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('Migrating'); + app.currentStatus = { detections: { strelka: { importing: true, migrating: false, syncing: true }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('Importing'); + app.currentStatus = { detections: { strelka: { importing: true, migrating: false, syncing: false }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('ImportPending'); + app.currentStatus = { detections: { strelka: { importing: false }}}; + expect(app.getDetectionEngineStatus('strelka')).toBe('Healthy'); +}); + +test('isAttentionNeeded', () => { + app.connected = true; + app.currentStatus = { + detections: { + elastalert: { + integrityFailure: false, + syncFailure: false, + migrationFailure: false, + }, + strelka: { + integrityFailure: false, + syncFailure: false, + migrationFailure: false, + }, + suricata: { + integrityFailure: false, + syncFailure: false, + migrationFailure: false, + }, + }, + alerts: { + newCount: 0, + }, + grid: { + unhealthyNodeCount: 0, + }, + }; + expect(app.isAttentionNeeded()).toBe(false); + + // Attention when unable to connect to server + app.connected = false; + expect(app.isAttentionNeeded()).toBe(true); + app.connected = true; + + // Attention when unhealthy grid count > 0 + app.currentStatus.grid.unhealthyNodeCount = 1 + expect(app.isAttentionNeeded()).toBe(true); + app.currentStatus.grid.unhealthyNodeCount = 0 + + // Attention when new alert count > 0 + app.currentStatus.alerts.newCount = 1 + expect(app.isAttentionNeeded()).toBe(true); + app.currentStatus.alerts.newCount = 0 + + // Attention when detections engines unhealthy + app.currentStatus.detections.elastalert.syncFailure = true; + expect(app.isAttentionNeeded()).toBe(true); + app.currentStatus.detections.elastalert.syncFailure = false; + + // Back to normal + expect(app.isAttentionNeeded()).toBe(false); +}) diff --git a/html/js/i18n.js b/html/js/i18n.js index 1fc197c7d..f4a3dab80 100644 --- a/html/js/i18n.js +++ b/html/js/i18n.js @@ -8,6 +8,8 @@ const i18n = { translations: { "en-US": { __missing__: '*Missing', // This should be sufficiently unique to avoid collisions with actual values + __soc_import__: 'SOC Import', + __custom__: 'Custom', accept: 'Accept', accepted: 'Accepted', acceptingMinionsTakesTime: 'Accepting new minions can take 1-2 minutes to complete.', @@ -38,6 +40,8 @@ const i18n = { actionProcessAncestorsHelp: 'Show all parent processes for this process', actionProcessInfo: 'Process Info', actionProcessInfoHelp: 'Show all logs for this process', + actionRelatedAlerts: 'Related Alerts', + actionRelatedAlertsHelp: 'Find alerts related to this detection', actionSublime: 'Sublime Platform Email Review', actionSublimeHelp: 'Review email in Sublime Platform', actionSuccess: 'Action completed: ', @@ -182,6 +186,8 @@ const i18n = { config: 'Configuration', configTitle: 'Grid Configuration', configQLGridTitle: 'Grid Administration Quick Links', + configQLElasticsearchDelete: 'Elasticsearch ILM Deletion', + configQLElasticsearchHeader: 'Elasticsearch', configQLNTP: 'Specify custom Network Time Protocol server(s)', configQLNTPHeader: 'NTP', configQLFirewallHeader: 'Firewall', @@ -195,8 +201,6 @@ const i18n = { configQLNIDSHeader: 'NIDS', configQLNIDSRuleset: 'Change NIDS Ruleset', configQLNIDSOinkCode: 'Paid NIDS Rulesets Registration Code (oinkcode)', - configQLSIDS: 'SIDS - ENABLE | DISABLE | MODIFY', - configQLSIDSThresholding: 'SIDS - Thresholding', configQLAnalystTitle: 'Analyst Quick Links', configQLSuricataHeader: 'Suricata', configQLHomeNet: 'Suricata Home Networks', @@ -276,13 +280,38 @@ const i18n = { description: 'Description', destination: 'Destination', details: 'Details', + detection: 'Detection', detections: 'Detections', + detectionDirtyTitle: 'Unsaved Changes to Source', + detectionDirtyBody: 'The Detection Source has unsaved changes. Would you like to save them first?', detectionsExcludeToggle: 'Exclude Detections data', detectionComments: 'Operational Notes', + detectionConfirmDelete: 'Confirm Delete Detection', + detectionConfirmDeleteHelp: 'Deleting a detection is permanent and there is no undo for this operation. Consider disabling this detection, if it may be needed at a later time.

Proceed and delete this detection?', detectionDefaultTitle: 'Detection title not yet provided - click here to update this title', detectionDefaultDescription: 'Detection description not yet provided', + detectionDeleteSuccessful: 'Detection deleted. The deleted detection will still appear below until the list is refreshed.', detectionDescription: 'Detection Description', detectionEnabled: 'Enabled', + detectionEngineHealthy: 'OK', + detectionEngineHealthyHelp: 'No problems detected with this engine\'s rules.', + detectionEngineImportPending: 'Import Pending', + detectionEngineImportPendingHelp: 'Rule import is pending and will begin soon.', + detectionEngineImporting: 'Importing', + detectionEngineImportingHelp: 'Importing rules occurs once, to convert all rules from the old Playbook system to the new Detections module.', + detectionEngineIntegrityFailure: 'Rule Mismatch', + detectionEngineIntegrityFailureHelp: 'Click to hunt for SOC logs that might provide more information about the mismatch.', + detectionEngineMigrating: 'Migrating', + detectionEngineMigratingHelp: 'Rule migration takes place after upgrading Security Onion to a new version.', + detectionEngineMigrationFailure: 'Migration Failed', + detectionEngineMigrationFailureHelp: 'Rule migration aborted due to errors. Click to hunt for related SOC logs.', + detectionEngineSyncFailure: 'Sync Failed', + detectionEngineSyncFailureHelp: 'Errors occurred during the most recent rule sync. Click to hunt for related SOC logs.', + detectionEngineSyncing: 'Synchronizing', + detectionEngineSyncingHelp: 'Rules synchronize periodically to ensure the latest rulesets are applied to this grid.', + detectionEngineUnknown: 'Pending', + detectionEngineUnknownHelp: 'A status update should be provided by the SOC server momentarily', + detectionId: 'Detection Id', detectionLogic: 'Detection Logic', detectionSeverity: 'Severity', @@ -291,6 +320,7 @@ const i18n = { detectionType: 'Detection Type', disable: 'Disable', disabled: 'Disabled', + disabledFailedSync: 'The detection was saved but synchronization failed. The detection has been disabled. Check SOC logs for details.', disconnected: 'Disconnected from manager', diskUsageElastic: 'Elastic Storage Used', diskUsageInfluxDb: 'InfluxDB Storage Used', @@ -320,6 +350,7 @@ const i18n = { endTime: 'Filter End', endTimeHelp: 'Filter end time in RFC 3339 format (Ex: 2020-10-16 15:30:00.230-04:00). Unused for imported PCAPs.', engine: 'Engine', + engineSelect: 'Please select an engine to synchronize.', eps: 'EPS', epsProduction: 'Production EPS:', epsConsumption: 'Consumption EPS:', @@ -371,6 +402,11 @@ const i18n = { 'field_so_detection.severity': 'Severity', 'field_so_detection.isEnabled': 'Enabled', 'field_so_detection.title': 'Title', + 'field_so_detection.ruleset': 'Ruleset', + 'field_so_detection.category': 'Category', + 'field_so_detection.product': 'Logsource - Product', + 'field_so_detection.service': 'Logsource - Service', + 'field_so_detection.overrides.type': 'Override Type', field_count: 'Count', field_soc_id: 'Event ID', field_soc_timestamp: 'Timestamp', @@ -422,7 +458,7 @@ const i18n = { gridMemberTest: "Test Grid Sensor", gridMemberTestConfirmTitle: "Confirm Sensor Test", gridMemberTestConfirmHelp: "Testing this sensor will cause test data to be ingested into Security Onion. This can be useful for validating proper sensor operation. Test data will remain loaded until the index is curated. This operation can take several minutes to complete. Do you want to continue?", - gridMemberTestSuccess: "Test data has been replayed to the monitoring interface, and is now being ingested into the search nodes. This may take a few minutes depending on grid resources.", + gridMemberTestSuccess: "Test data has been replayed to the monitoring interface, and is now being ingested into the search nodes. This may take a few minutes depending on grid resources. Note that this action could trigger alerts on other enterprise monitoring systems, depending on your network and interface configuration.", gridMemberUploadTitleBoth: 'Upload a PCAP or EVTX File', gridMemberUploadTitleEvtx: 'Upload a EVTX File', gridMemberUploadTitlePcap: 'Upload a PCAP File', @@ -430,8 +466,7 @@ const i18n = { gridMemberUploadConflict: 'A file with that name is currently being imported. Please wait for the current import to complete before trying again.', gridMemberUploadFailure: 'Something went wrong while uploading the file. The file was not imported.', gridMemberImportNoChanges: 'A recent import made no changes.', - // note: must replace <[url]> with a link to the results page - gridMemberImportSuccess: 'A recent import has completed and the results will be available in Dashboards momentarily.', + gridMemberImportSuccess: 'A recent import has completed and the results will be available in Dashboards momentarily.', gridMemberRestartConfirmTitle: 'Reboot Node', gridMemberRestartConfirmHelp: 'Rebooting a node may be required for various reasons, such as if the node has installed new kernel updates.

The grid may show a fault for several minutes while the node reboots and starts the Security Onion services.

⚠️ Rebooting the manager node will temporarily prevent access to this web application and may show an error similar to 502 Bad Gateway. Wait a few minutes and then refresh the browser window to regain access.', gridMemberRestartSuccess: 'Successfully issued a reboot request to the node.', @@ -458,6 +493,8 @@ const i18n = { huntForEvidence: 'Hunt for this observable value', huntHelp: 'Start a new hunt based on the current filters', id: 'ID', + idMissingErr: 'This Sigma rule is missing its public Id. A public Id is required.', + idMismatchErr: "The Id in this Sigma rule must match the detection's public Id.", importId: 'Import ID', importIdHelp: 'UUID value that is output from so-import-pcap. Only needed for imported PCAPs.', index: "Index", @@ -522,6 +559,9 @@ const i18n = { loginTitle: 'Login to Security Onion', logout: 'Logout', logoutFailure: 'Unable to initiate logout. Ensure server is accessible.', + manualSyncFull: 'Full Update', + manualSyncUpdate: 'Differential Update', + manualSyncHint: 'Select an engine to synchronize', markdownFormattingSupported: 'Markdown formatting supported', maximize: 'Maximize View (ESC to cancel)', maxUploadSize: 'Maximum upload size', @@ -531,6 +571,7 @@ const i18n = { memUsageAbbr: 'Mem', message: 'Message', minutes: 'minutes', + missingPublicIdErr: "This detection is missing its public Id. A public Id is required.", model: 'Model', module: 'Module', months: 'months', @@ -565,6 +606,7 @@ const i18n = { none: 'None', note: 'Note', notFound: 'The selected item no longer exists', + ntf: 'Notifications', number: 'Num', numericOps: 'Numeric Ops', odc: 'Open ID Connect', @@ -585,6 +627,8 @@ const i18n = { order: 'Order', osUptime: 'OS Uptime', other: 'Other', + overrideDeleteHelp: 'Delete this rule override', + overrideDocumentationHelp: 'Click for more information', overrideExpand: 'Show all override fields', overview: 'Overview', owner: 'Owner', @@ -601,13 +645,6 @@ const i18n = { passwordRequired: 'A password must be specified.', passwordReset: 'Change Password', passwordNeedsChanged: 'User has not yet changed their password', - playbooks: 'Playbooks', - playbookAddContext: 'Provide optional context', - playbookDatasources: 'The data sources that can be used to answer the question', - playbookDescription: 'Playbook Description', - playbookTitle: 'Playbook Title', - playbookMechanism: 'Mechanism', - playbookQuestionHint: 'The investigative question written in plain language for human consumption, in the form of a question', profileDetails: 'Profile Details', profileInstructions: 'You may be prompted to login again when updating your profile. This is a security measure to protect your account.', pcap: 'PCAP', @@ -619,6 +656,7 @@ const i18n = { protocol: 'Protocol', protocolHelp: 'Optional protocol, such as "icmp" or "tcp".', publicId: 'Public Id', + publicIdConflictErr: 'A detection with this public Id already exists. Please choose a different public Id.', queriesHelp: 'Choose from several pre-defined queries', queryHelp: 'Specify a query in Onion Query Language (OQL)', quickActions: 'Actions', @@ -659,6 +697,7 @@ const i18n = { ruleMinLen: 'The provided value is too short', ruleMaxLen: 'The provided value is too long', rulePassBadChars: 'The password must not contain the following characters: " \' $ & !', + rules: 'Rules', ruleset: 'Ruleset', save: 'Save', saveSuccess: 'Save successful!', @@ -678,10 +717,18 @@ const i18n = { settingCategory_ui: 'User Interface', settingConfirmCancel: 'Unsaved Changes', settingConfirmCancelHelp: 'Discard unsaved changes?', + settingConfirmReset: 'Reset Value', + settingConfirmResetHelp: 'Delete this custom setting, and reset to default if available?', settingAdvanced: 'Provide optional, custom configuration in YAML format. Note that improper customizations often are the cause of grid malfunctions.', settingDefault: 'Default Value', settingDeleted: 'Setting deleted/reset successfully. Changes typically apply within 15 minutes.', settingDeleteError: 'Setting could not be deleted.', + settingDuplicate: 'Duplicate', + settingDuplicateCreate: 'Create Setting', + settingDuplicateInvalid: 'The chosen duplicate ID is already taken. Change the ID to a unique value and try again.', + settingDuplicateName: 'Name of new setting', + settingDuplicateNameInvalid: 'Must be between 3 and 50 characters in length, and must only contain letters, underscores, and digits', + settingDuplicateHelp: 'This config setting can be duplicated if additional or explicitly-named settings are needed. Clicking the button below will result in a new advanced setting with the given name. IMPORTANT: The new setting will not be saved to the server until its value has been modified. Setting names must be unique. Duplicated settings cannot be removed or renamed via the SOC user interface. Since duplicated settings are considered advanced settings, future access to the settings via this Configuration screen will require enabling the Advanced option.', settingHelp: 'View documentation or related information for this setting', settingSelect: 'Select a setting from the tree view on the left or the quick links on the right.', settingGlobal: 'Current Grid Value', @@ -755,13 +802,14 @@ const i18n = { showBarChart: 'Show bar chart', showSankeyChart: 'Show Sankey diagram', showTable: 'Show table', - sidMissingErr: "This suricata rule is missing it's SID.", + sidMissingErr: "This Suricata rule is missing its SID.", sidMultipleErr: 'Suricata rules can only specify one SID.', - sidMismatchErr: "The SID in this suricata rule must match the detection's Public ID.", + sidMismatchErr: "The SID in this Suricata rule must match the detection's public Id.", signature: 'Signature', + socExcludeToggle: 'Exclude SOC logs', socId: 'SOC Id', socUrl: 'SOC Url', - socExcludeToggle: 'Exclude SOC logs', + socUser: 'SOC User', sortedBy: 'Sort:', sortInclude: "Sort By", sortIncludeHelp: "Add as a sort-by field", @@ -774,6 +822,8 @@ const i18n = { standardMetrics: 'Basic Metrics', startEndNumericErr: 'Start and End values must be numeric.', startEndOrderErr: 'Start value must come before End value.', + startSyncFull: 'Started a full sync of {engine} detections.', + startSyncUpdate: 'Started an update sync of {engine} detections.', status: 'Status', stenoLoss: 'Stenographer Loss', stenoLossAbbr: 'Steno Loss', @@ -782,6 +832,10 @@ const i18n = { suricataLoss: 'Suricata Loss', suricataLossAbbr: 'Suri Loss', swapUsage: 'Swap Usage', + syncSuccess: 'Synchronized {engine} community rules successfully.', + syncPartialSuccess: 'Synchronized {engine} community rules with some errors. Check SOC logs for details.', + syncFailure: 'Something went wrong attempting to synchronize {engine} community rules. Check SOC logs for details.', + systemUser: "System", tags: 'Tags', thresholdType: 'Threshold Type', throttledLogin: 'Excessive login requests detected. Login requests can resume momentarily.', @@ -807,8 +861,6 @@ const i18n = { toolKibanaHelp: 'Elasticsearch User Interface', toolNavigator: 'Navigator', toolNavigatorHelp: 'MITRE ATT@CK Navigator', - toolPlaybook: 'Playbook', - toolPlaybookHelp: 'Detection Playbook', toolTheHive: 'TheHive', toolTheHiveHelp: 'Case Management', totp: 'Time-based One-Time Password (TOTP)', @@ -908,6 +960,13 @@ const i18n = { ERROR_SALT_SEND_FILE: 'Unable to send file to minion; ensure that salt is running on the manager node and check salt logs.', ERROR_SALT_IMPORT: 'Unable to import file on minion; ensure that salt is running on the manager node and check salt logs.', ERROR_SALT_STATE: 'Unable to sync settings. Ensure that salt is running on the manager node and check salt logs.', + + // correct casing + cc_elastalert: 'ElastAlert', + cc_sigma: 'Sigma', + cc_strelka: 'Strelka', + cc_suricata: 'Suricata', + cc_yara: 'YARA', }, }, diff --git a/html/js/routes/config.js b/html/js/routes/config.js index 631db5288..825144202 100644 --- a/html/js/routes/config.js +++ b/html/js/routes/config.js @@ -10,6 +10,7 @@ routes.push({ path: '/config', name: 'config', component: { i18n: this.$root.i18n, settings: [], search: "", + searchFilter: null, autoExpand: false, autoSelect: "", form: { @@ -17,6 +18,10 @@ routes.push({ path: '/config', name: 'config', component: { key: "", value: "", }, + duplicate_id_rules: [ + value => !!value || this.$root.i18n.required, + value => (!!value && /^[a-zA-Z0-9_]{3,50}$/.test(value)) || this.$root.i18n.settingDuplicateNameInvalid, + ], selectedNode: null, cancelDialog: false, @@ -31,6 +36,12 @@ routes.push({ path: '/config', name: 'config', component: { settingsAvailable: 0, showDefault: false, nextStopId: null, + showDuplicate: false, + duplicateId: null, + duplicateIdValid: false, + resetSetting: null, + resetNodeId: null, + confirmResetDialog: false, }}, mounted() { this.processRouteParameters(); @@ -63,6 +74,7 @@ routes.push({ path: '/config', name: 'config', component: { this.autoExpand = true; this.search = this.$route.query.s; } + this.applySearchFilter(); }, findActiveSetting() { if (this.active.length > 0) { @@ -74,8 +86,12 @@ routes.push({ path: '/config', name: 'config', component: { } return null; }, + applySearchFilter() { + this.searchFilter = this.search; + }, clearFilter() { this.search = ""; + this.searchFilter = ""; }, filter(item, search, textKey) { if (!search) return true; @@ -160,6 +176,7 @@ routes.push({ path: '/config', name: 'config', component: { default: null, defaultAvailable: false, readonly: setting.readonly, + readonlyUi: setting.readonlyUi, sensitive: setting.sensitive, regex: setting.regex, regexFailureMessage: setting.regexFailureMessage, @@ -167,6 +184,7 @@ routes.push({ path: '/config', name: 'config', component: { helpLink: setting.helpLink, advanced: setting.advanced, syntax: setting.syntax, + duplicates: setting.duplicates, }; this.merge(created, setting); return created; @@ -308,6 +326,8 @@ routes.push({ path: '/config', name: 'config', component: { } this.recomputeAvailableNodes(this.findActiveSetting()); this.activeBackup = [...this.active]; + this.showDuplicate = false; + this.showDefault = false; window.scrollTo(0,0); }, cancel(force) { @@ -346,26 +366,37 @@ routes.push({ path: '/config', name: 'config', component: { return true; }, - async remove(setting, nodeId) { - if (setting) { + remove(setting, nodeId) { + this.resetSetting = setting; + this.resetNodeId = nodeId; + this.confirmResetDialog = true; + }, + cancelRemove() { + this.resetSetting = null; + this.resetNodeId = null; + this.confirmResetDialog = false; + }, + async confirmRemove() { + this.confirmResetDialog = false; + if (this.resetSetting) { this.$root.startLoading(); try { - await this.$root.papi.delete('config/', { params: { id: setting.id, minion: nodeId }}); + await this.$root.papi.delete('config/', { params: { id: this.resetSetting.id, minion: this.resetNodeId }}); - if (nodeId) { + if (this.resetNodeId) { // Rebuild UI as needed const newMap = new Map(); - for (const [key, value] of setting.nodeValues.entries()) { - if (key != nodeId) { + for (const [key, value] of this.resetSetting.nodeValues.entries()) { + if (key != this.resetNodeId) { newMap.set(key, value); } } - setting.nodeValues.clear(); - setting.nodeValues = newMap; + this.resetSetting.nodeValues.clear(); + this.resetSetting.nodeValues = newMap; this.recomputeAvailableNodes(this.findActiveSetting()); } else { - this.reset(setting); - setting.value = setting.default; + this.reset(this.resetSetting); + this.resetSetting.value = this.resetSetting.default; } this.countCustomized(); @@ -377,6 +408,7 @@ routes.push({ path: '/config', name: 'config', component: { } this.$root.stopLoading(); } + this.cancelRemove(); this.cancel(true); }, async save(setting, nodeId) { @@ -473,5 +505,34 @@ routes.push({ path: '/config', name: 'config', component: { }); this.availableNodes = eligible.map(n => { return { text: n.name + " (" + n.role + ")", value: n.id } }); }, + toggleDuplicate(setting) { + this.duplicateId = this.suggestDuplicateName(setting); + this.showDuplicate = !this.showDuplicate; + }, + suggestDuplicateName(setting) { + return setting.name + "_dup"; + }, + duplicate(setting) { + const new_name = this.duplicateId; + var new_id = setting.id.substring(0, setting.id.lastIndexOf(setting.name)); + new_id += new_name; + this.$root.startLoading(); + const found = this.settings.find(s => s.id == new_id); + this.$root.stopLoading(); + if (found) { + this.$root.showWarning(this.i18n.settingDuplicateInvalid); + return + } + var new_setting = structuredClone(setting); + new_setting.id = new_id; + new_setting.name = new_name; + this.settings.push(new_setting); + this.settings.sort((a,b) => { if (a.id > b.id) return 1; else if (a.id < b.id) return -1; else return 0 }); + this.refreshTree(); + this.active = [new_id] + }, + isReadOnly(item) { + return item.readonly || item.readonlyUi; + }, } }}); diff --git a/html/js/routes/config.test.js b/html/js/routes/config.test.js index 6b7a39f30..160833ce1 100644 --- a/html/js/routes/config.test.js +++ b/html/js/routes/config.test.js @@ -52,6 +52,7 @@ test('loadData', async () => { "default": null, "defaultAvailable": false, "description": "Nearby", + "duplicates": undefined, "file": undefined, "global": false, "helpLink": undefined, @@ -61,6 +62,7 @@ test('loadData', async () => { "node": undefined, "nodeValues": m1, "readonly": undefined, + "readonlyUi": undefined, "regex": "True|False", "regexFailureMessage": "Wrong!", "sensitive": undefined, @@ -73,6 +75,7 @@ test('loadData', async () => { "default": undefined, "defaultAvailable": undefined, "description": "NADA", + "duplicates": undefined, "file": undefined, "global": undefined, "helpLink": undefined, @@ -82,6 +85,7 @@ test('loadData', async () => { "node": false, "nodeValues": new Map(), "readonly": undefined, + "readonlyUi": undefined, "regex": undefined, "regexFailureMessage": undefined, "sensitive": undefined, @@ -94,6 +98,7 @@ test('loadData', async () => { "default": undefined, "defaultAvailable": undefined, "description": "Cocoa", + "duplicates": undefined, "file": undefined, "global": undefined, "helpLink": undefined, @@ -103,6 +108,7 @@ test('loadData', async () => { "node": false, "nodeValues": new Map(), "readonly": undefined, + "readonlyUi": undefined, "regex": undefined, "regexFailureMessage": undefined, "sensitive": undefined, @@ -122,6 +128,7 @@ test('loadData', async () => { "default": null, "defaultAvailable": false, "description": "Nearby", + "duplicates": undefined, "file": undefined, "global": false, "helpLink": undefined, @@ -131,6 +138,7 @@ test('loadData', async () => { "node": undefined, "nodeValues": m1, "readonly": undefined, + "readonlyUi": undefined, "regex": "True|False", "regexFailureMessage": "Wrong!", "sensitive": undefined, @@ -143,6 +151,7 @@ test('loadData', async () => { "default": undefined, "defaultAvailable": undefined, "description": "Cocoa", + "duplicates": undefined, "file": undefined, "global": undefined, "helpLink": undefined, @@ -152,6 +161,7 @@ test('loadData', async () => { "node": false, "nodeValues": new Map(), "readonly": undefined, + "readonlyUi": undefined, "regex": undefined, "regexFailureMessage": undefined, "sensitive": undefined, @@ -172,6 +182,7 @@ test('loadData', async () => { "default": undefined, "defaultAvailable": undefined, "description": "NADA", + "duplicates": undefined, "file": undefined, "global": undefined, "helpLink": undefined, @@ -181,6 +192,7 @@ test('loadData', async () => { "node": false, "nodeValues": new Map(), "readonly": undefined, + "readonlyUi": undefined, "regex": undefined, "regexFailureMessage": undefined, "sensitive": undefined, @@ -307,6 +319,7 @@ test('selectSetting', () => { expect(comp.activeBackup).toStrictEqual(["s-id"]); expect(comp.availableNodes).toStrictEqual([{text: "node2 (standalone)", value: "n2"}]); expect(comp.cancelDialog).toBe(false); + expect(comp.confirmResetDialog).toBe(false); }); test('cancel', () => { @@ -330,26 +343,60 @@ test('cancel', () => { expect(comp.form.key).toBe("cancel-id"); }); -test('remove', async () => { +test('remove', () => { + expect(comp.confirmResetDialog).toBe(false); + expect(comp.resetSetting).toBe(null); + expect(comp.resetNodeId).toBe(null); + comp.resetNodeId = "foo" + comp.resetSetting = "bar" + comp.confirmResetDialog = true + comp.remove("bar", "foo"); + expect(comp.confirmResetDialog).toBe(true); + expect(comp.resetSetting).toBe("bar"); + expect(comp.resetNodeId).toBe("foo"); +}); + +test('cancelReset', () => { + expect(comp.confirmResetDialog).toBe(false); + expect(comp.resetSetting).toBe(null); + expect(comp.resetNodeId).toBe(null); + comp.resetNodeId = "foo" + comp.resetSetting = "bar" + comp.confirmResetDialog = true + comp.cancelRemove("bar", "foo"); + expect(comp.confirmResetDialog).toBe(false); + expect(comp.resetSetting).toBe(null); + expect(comp.resetNodeId).toBe(null); +}); + +test('confirmRemove', async () => { setupSettings(); // No-op path + comp.remove(comp.settings[0], "nonexisting"); var mock = mockPapi("delete"); - await comp.remove(comp.settings[0], "nonexisting"); + await comp.confirmRemove(); var expectedNodeValues = new Map(); expectedNodeValues.set("n1", "123"); expectedNodeValues.set("n1a", "abc"); expect(comp.settings[0].nodeValues).toStrictEqual(expectedNodeValues); + expect(comp.resetSetting).toBe(null); + expect(comp.resetNodeId).toBe(null); + expect(comp.confirmResetDialog).toBe(false); expect(comp.cancelDialog).toBe(false); expect(comp.form.key).toBe(null); expect(mock).toHaveBeenCalledWith('config/', { params: {"id": "s-id", "minion": "nonexisting" }}); // Good path + comp.remove(comp.settings[0], "n1"); mock = mockPapi("delete"); - await comp.remove(comp.settings[0], "n1"); + await comp.confirmRemove(); expectedNodeValues = new Map(); expectedNodeValues.set("n1a", "abc"); expect(comp.settings[0].nodeValues).toStrictEqual(expectedNodeValues); + expect(comp.resetSetting).toBe(null); + expect(comp.resetNodeId).toBe(null); + expect(comp.confirmResetDialog).toBe(false); expect(comp.cancelDialog).toBe(false); expect(comp.form.key).toBe(null); expect(mock).toHaveBeenCalledWith('config/', { params: {"id": "s-id", "minion": "n1" }}); @@ -516,4 +563,63 @@ test('addToNode_Malformed', () => { comp.addToNode({name: 'test'}, {}, ['parent'], {name: 'test'}); }; expect(closure).toThrow("Setting name 'test' conflicts with another similarly named setting"); +}); + +test('toggleDuplicate', () => { + const setting = { + id: "a.b.c", + name: "c", + duplicates: true, + } + expect(comp.showDuplicate).toBe(false) + comp.toggleDuplicate(setting) + expect(comp.duplicateId).toBe("c_dup"); + expect(comp.showDuplicate).toBe(true) +}); + +test('duplicate', () => { + const setting = { + id: "a.b.c", + name: "c", + duplicates: true, + } + const setting2 = { + id: "a.b.c", + name: "c", + duplicates: true, + } + global.structuredClone = jest.fn().mockReturnValueOnce(setting2); + comp.settings = [setting]; + comp.duplicateId = "foo" + expect(comp.settings.length).toBe(1); + comp.duplicate(setting); + expect(comp.settings.length).toBe(2); + expect(comp.settings[1].id).toBe("a.b.foo"); + expect(comp.settings[1].name).toBe("foo"); +}); + +test('applySearchFilter', () => { + comp.search = "foo"; + comp.searchFilter = ""; + comp.applySearchFilter(); + expect(comp.searchFilter).toBe(comp.search); + comp.clearFilter(); + expect(comp.search).toBe(""); + expect(comp.searchFilter).toBe(""); +}); + +test('isReadOnly', () => { + const setting = { + id: "a1", + readonly: false, + readonlyUi: false, + }; + expect(comp.isReadOnly(setting)).toBe(false); + setting.readonly = true; + expect(comp.isReadOnly(setting)).toBe(true); + setting.readonly = false; + setting.readonlyUi = true; + expect(comp.isReadOnly(setting)).toBe(true); + setting.readonly = true; + expect(comp.isReadOnly(setting)).toBe(true); }); \ No newline at end of file diff --git a/html/js/routes/detection.js b/html/js/routes/detection.js index 5655e6dd3..1088f9cc6 100644 --- a/html/js/routes/detection.js +++ b/html/js/routes/detection.js @@ -55,8 +55,6 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { activeTab: '', sidExtract: /\bsid: ?['"]?(.*?)['"]?;/, // option severityExtract: /\bsignature_severity ['"]?(.*?)['"]?[,;]/, // metadata - authorExtract: /\bauthor: ?['"]?(.*?)['"]?;/, // option - authorMetaExtract: /\bauthor ['"]?(.*?)['"]?[,;]/, // metadata sortBy: 'createdAt', sortDesc: false, expanded: [], @@ -69,6 +67,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { ], zone: moment.tz.guess(), newOverride: null, + newOverrideValid: false, thresholdTypes: [ { value: 'threshold', text: 'Threshold' }, { value: 'limit', text: 'Limit' }, @@ -97,7 +96,6 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { history: [], extractedCreated: '', extractedUpdated: '', - extractedAuthor: '', comments: [], commentsTable: { showAll: false, @@ -121,6 +119,8 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { origComment: null, showSigmaDialog: false, convertedRule: '', + confirmDeleteDialog: false, + showDirtySourceDialog: false, }}, created() { this.onDetectionChange = debounce(this.onDetectionChange, 300); @@ -177,9 +177,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { try { const response = await this.$root.papi.get('detection/' + encodeURIComponent(this.$route.params.id)); - this.detect = response.data; - this.tagOverrides(); - this.loadAssociations(); + this.extractDetection(response); } catch (error) { if (error.response != undefined && error.response.status == 404) { this.$root.showError(this.i18n.notFound); @@ -190,6 +188,18 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.$root.stopLoading(); }, + extractDetection(response) { + this.detect = response.data; + delete this.detect.kind; + + this.tagOverrides(); + this.loadAssociations(); + this.origDetect = Object.assign({}, this.detect); + // Don't await the user details -- takes too long for the task scheduler to + // complete all these futures when looping across hundreds of records. Let + // the UI update as they finish, for a better user experience. + this.$root.populateUserDetails(this.detect, "userId", "userName"); + }, loadAssociations() { this.extractSummary(); this.extractReferences(); @@ -278,11 +288,11 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { }, isValidUrl(urlString) { var urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name - '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path - '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string - '(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string + '(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator return !!urlPattern.test(urlString); }, fixProtocol(url) { @@ -392,7 +402,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.extractedLogic = jsyaml.dump({ logsource: logSource, detection: detection }).trim(); }, extractDetails() { - this.extractedAuthor = this.extractedCreated = this.extractedUpdated = ''; + this.extractedCreated = this.extractedUpdated = ''; switch (this.detect.engine) { case 'suricata': @@ -434,22 +444,10 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.extractedUpdated = date[0]; } } - - if (md.indexOf('author') > -1) { - this.extractedAuthor = md.replace('author', '').trim(); - } } }, extractStrelkaDetails() { - const authorExtractor = /^\s*author\s*=\s*"(.*)"/im; const dateExtractor = /^\s*date\s*=\s*"(.*)"/im; - - const authorMatch = authorExtractor.exec(this.detect.content); - - if (authorMatch) { - this.extractedAuthor = authorMatch[1]; - } - const dateMatch = dateExtractor.exec(this.detect.content); if (dateMatch) { @@ -459,16 +457,19 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { extractElastAlertDetails() { const yaml = jsyaml.load(this.detect.content, { schema: jsyaml.FAILSAFE_SCHEMA }); - this.extractedAuthor = yaml['author']; this.extractedCreated = yaml['date']; this.extractedUpdated = yaml['modified']; }, async loadHistory() { - const route = this; - const id = route.$route.params.id; + const id = this.$route.params.id; + const response = await this.$root.papi.get(`detection/${id}/history`); if (response && response.data) { this.history = response.data; + + for (var i = 0; i < this.history.length; i++) { + this.$root.populateUserDetails(this.history[i], "userId", "owner"); + } } }, getDefaultPreset(preset) { @@ -486,7 +487,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { case 'severity': case 'engine': case 'language': - return this.capitalizeOptions(this.presets[kind].labels); + return this.translateOptions(this.presets[kind].labels); default: return this.presets[kind].labels; } @@ -503,14 +504,8 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { return []; }, - capitalizeOptions(opts) { - return opts.map(opt => { - const cap = opt.charAt(0).toUpperCase() + opt.slice(1).toLowerCase(); - return { - text: cap, - value: opt, - } - }) + translateOptions(opts) { + return opts.map(opt => this.$root.correctCasing(opt)) }, requestRules(rules) { if (this.detect.isCommunity) { @@ -528,12 +523,17 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { isNew() { return this.$route.params.id === 'create'; }, + isDetectionSourceDirty() { + return this.detect.content != this.origDetect.content; + }, cancelDetection() { if (this.isNew()) { this.$router.push({name: 'detections'}); } else { - this.detect = this.origDetect; + this.detect = Object.assign({}, this.origDetect); } + + this.showDirtySourceDialog = false; }, async startEdit(target, field) { if (this.curEditTarget === target) return; @@ -571,7 +571,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { isEdit(target) { return this.curEditTarget === target; }, - async stopEdit(commit) { + stopEdit(commit) { if (!commit) { this.detect[this.editField] = this.origValue; } @@ -581,27 +581,39 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.editField = null; if (commit && !this.isNew()) { - this.saveDetection(false); + this.saveDetection(false).then(() => { + this.curEditTarget = null; + }); } }, - async saveDetection(createNew) { + revertEnabled() { + const route = this; + this.$nextTick(() => { + route.detect.isEnabled = route.origDetect.isEnabled; + }); + }, + async saveDetection(createNew, skipSourceCheck) { if (this.curEditTarget !== null) this.stopEdit(true); + if (!this.isNew() && !skipSourceCheck && this.isDetectionSourceDirty()) { + this.showDirtySourceDialog = true; + this.revertEnabled(); + return; + } + + this.showDirtySourceDialog = false; if (this.isNew()) { this.$refs.detection.validate(); if (!this.editForm.valid) return; - - let author = [this.$root.user.firstName, this.$root.user.lastName].filter(x => x).join(' '); - this.detect.author = author; } let err; switch (this.detect.engine) { - case 'yara': - err = this.validateYara(); + case 'strelka': + err = this.validateStrelka(); break; - case 'sigma': - err = this.validateSigma(); + case 'elastalert': + err = this.validateElastAlert(); break; case 'suricata': err = this.validateSuricata(); @@ -610,6 +622,8 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { if (err) { this.$root.showError(err); + this.revertEnabled(); + return; } @@ -622,7 +636,9 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { if (createNew) { response = await this.$root.papi.post('/detection', this.detect); } else { - response = await this.$root.papi.put('/detection', this.detect); + response = await this.$root.papi.put('/detection', this.detect, { + validateStatus: (s) => (s >= 200 && s < 300) + }); } // get any expanded overrides before updating this.detect @@ -631,41 +647,82 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { index = this.expanded[0].index; } - this.detect = response.data; - this.tagOverrides(); - this.origDetect = Object.assign({}, this.detect); + this.extractDetection(response); - // reinstate expanded override - if (index != -1 && this.detect.overrides && this.detect.overrides.length > index) { - this.expand(this.detect.overrides[index]); + switch (response.status) { + case 206: + this.$root.showWarning(this.i18n.disabledFailedSync); + break; + default: + this.$root.showTip(this.i18n.saveSuccess); + break; } - this.$root.showTip(this.i18n.saveSuccess); - if (createNew) { this.$router.push({ name: 'detection', params: { id: response.data.id } }); } + return true; + } catch (error) { - this.$root.showError(error); + switch (error.response.status) { + case 409: + this.$root.showWarning(this.i18n.publicIdConflictErr); + break; + default: + this.$root.showError(error); + break; + } + + this.revertEnabled(); } finally { this.$root.stopLoading(); } }, async duplicateDetection() { const response = await this.$root.papi.post('/detection/' + encodeURIComponent(this.$route.params.id) + '/duplicate'); - this.$router.push({name: 'detection', params: {id: response.data.id}}); + this.extractDetection(response); + + this.$router.push({ name: 'detection', params: { id: response.data.id } }); }, - async deleteDetection() { - await this.$root.papi.delete('/detection/' + encodeURIComponent(this.$route.params.id)); - this.$router.push({ name: 'detections' }); + deleteDetection() { + this.confirmDeleteDialog = true; }, - validateYara() { - return null; + cancelDeleteDetection() { + this.confirmDeleteDialog = false; }, - validateSigma() { + async confirmDeleteDetection() { + this.cancelDeleteDetection(); + try { + this.$root.startLoading(); + await this.$root.papi.delete('/detection/' + encodeURIComponent(this.$route.params.id)); + this.$router.push({ name: 'detections' }); + this.$root.showTip(this.i18n.detectionDeleteSuccessful); + } catch (error) { + this.$root.showError(error); + } finally { + this.$root.stopLoading(); + } + }, + validateStrelka() { return null; }, + validateElastAlert() { + try { + const id = this.extractElastAlertPublicID(); + if (!id) { + throw this.i18n.idMissingErr; + } + + if (this.detect.publicId && this.detect.publicId !== id) { + throw this.i18n.idMismatchErr; + } + + return null; + } catch (e) { + return e; + } + }, validateSuricata() { try { const sid = this.extractSuricataPublicID(); @@ -720,27 +777,12 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.detect.severity = sev; }, - extractAuthor() { - let author = this.detect.author; - switch (this.detect.engine) { - case 'suricata': - try { - const a = this.extractSuricataAuthor(); - if (a) author = a; - } catch {} - break; - case 'elastalert': - try { - const a = this.extractElastAlertAuthor(); - if (a) author = a; - } catch {} - break; - } - - this.detect.author = author; - }, extractSuricataPublicID() { const results = this.sidExtract.exec(this.detect.content); + if (results === null || results.length < 2) { + throw this.i18n.sidMissingErr; + } + return results[1]; }, extractSuricataSeverity() { @@ -757,17 +799,6 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { return sev; }, - extractSuricataAuthor() { - // do suricata rules even have a place for an author? - - // first look for an option labeled author - const author = this.authorExtract.exec(this.detect.content); - if (author && author.length >= 2) return author[1]; - - // if no option, check metadata for a field labeled author - const authorMeta = this.authorMetaExtract.exec(this.detect.content); - if (authorMeta && authorMeta.length >= 2) return authorMeta[1]; - }, extractElastAlertPublicID() { const yaml = jsyaml.load(this.detect.content, {schema: jsyaml.FAILSAFE_SCHEMA}); return yaml['id']; @@ -782,15 +813,10 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { } } }, - extractElastAlertAuthor() { - const yaml = jsyaml.load(this.detect.content, {schema: jsyaml.FAILSAFE_SCHEMA}); - return yaml['author']; - }, onDetectionChange() { if (this.detect.engine) { this.extractPublicID(); this.extractSeverity(); - this.extractAuthor(); } }, saveSetting(name, value, defaultValue = null) { @@ -830,16 +856,6 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { } return b < a ? -1 : 1; }, - expand(item) { - if (this.isExpanded(item)) { - this.expanded = []; - } else { - this.expanded = [item]; - } - }, - isExpanded(item) { - return (this.expanded.length > 0 && this.expanded[0] === item); - }, createNewOverride() { this.newOverride = { type: null, @@ -864,7 +880,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { ]; case 'elastalert': return [ - { value: 'custom filter', text: 'Custom Filter' } + { value: 'customFilter', text: 'Custom Filter' } ]; } @@ -927,16 +943,21 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.$refs.OverrideCreate.reset(); this.newOverride = null; }, - addNewOverride() { + async addNewOverride() { if (!this.newOverride) return; if (!this.detect.overrides) { this.detect.overrides = []; } + this.newOverride.isEnabled = true; + this.detect.overrides.push(this.newOverride); - this.saveDetection(false); + const result = await this.saveDetection(false); + if (!result) { + this.detect.overrides.pop(); + } this.newOverride = null; }, async startOverrideEdit(target, override, field) { @@ -959,13 +980,16 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { isOverrideEdit(target) { return this.curOverrideEditTarget === target; }, - async stopOverrideEdit(commit) { + stopOverrideEdit(commit) { if (commit && this.$refs[this.curOverrideEditTarget].hasError) return; if (!commit) { this.editOverride[this.overrideEditField] = this.origOverrideValue; } else { - this.saveDetection(false); + this.$nextTick(async () => { + await this.saveDetection(false); + this.curOverrideEditTarget = null; + }); } this.curOverrideEditTarget = null; @@ -978,21 +1002,11 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.saveDetection(false); }, canAddOverride() { - if (this.detect.engine === 'elastalert') { - if (this.detect.overrides && this.detect.overrides.length > 0) { - for (let i = 0; i < this.detect.overrides.length; i++) { - if (this.detect.overrides[i].type === 'custom filter') { - // elastalert detections that already have a custom filter - // cannot have any other custom filter overrides - return false; - } - } - } - } else if (this.detect.engine === 'strelka') { - return false; - } - - return true; + return this.detect.engine !== 'strelka'; + }, + canConvert() { + let lang = this.detect.language || ''; + return lang.toLowerCase() === 'sigma'; }, tagOverrides() { if (this.detect.overrides) { @@ -1003,26 +1017,6 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { this.detect.overrides = []; } }, - isExpanded(row) { - const expanded = this.historyTableOpts.expanded; - for (var i = 0; i < expanded.length; i++) { - if (expanded[i].id == row.id) { - return true; - } - } - return false; - }, - async expandRow(row) { - const expanded = this.historyTableOpts.expanded; - for (var i = 0; i < expanded.length; i++) { - if (expanded[i].id == row.id) { - expanded.splice(i, 1); - return; - } - } - - expanded.push(row); - }, prepareForInput(id) { const el = document.getElementById(id) el.scrollIntoView() @@ -1180,7 +1174,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { async convertDetection(content) { this.$root.startLoading(); try { - const response = await this.$root.papi.post('detection/convert', { content: content }); + const response = await this.$root.papi.post('detection/convert', this.detect); if (response && response.data) { this.convertedRule = response.data.query; this.showSigmaDialog = true; @@ -1208,6 +1202,13 @@ routes.push({ path: '/detection/:id', name: 'detection', component: { const compress = LZString.compressToEncodedURIComponent(query); const url = `/kibana/app/dev_tools#/console?load_from=data:text/plain,${compress}`; window.open(url, '_blank'); - } + }, + isFieldValid(refName) { + const ref = this.$refs[refName]; + if (ref) { + return ref.valid; + } + return true; + }, } }}); diff --git a/html/js/routes/detection.test.js b/html/js/routes/detection.test.js index 201649a7a..8e55af282 100644 --- a/html/js/routes/detection.test.js +++ b/html/js/routes/detection.test.js @@ -35,7 +35,6 @@ test('extract suricata', () => { expect(comp.extractedLogic).toBe('any any <> any any'); expect(comp.extractedCreated).toBe('2020-01-01'); expect(comp.extractedUpdated).toBe('2020-01-02'); - expect(comp.extractedAuthor).toBe('Bob'); }); test('extract strelka', () => { @@ -59,7 +58,6 @@ test('extract strelka', () => { expect(comp.extractedLogic).toBe('strings:\n$a = "test"\ncondition:\n$a'); expect(comp.extractedCreated).toBe('2020-01-01'); expect(comp.extractedUpdated).toBe(''); - expect(comp.extractedAuthor).toBe('Bob'); }); test('extract elastalert', () => { @@ -83,7 +81,6 @@ test('extract elastalert', () => { expect(comp.extractedLogic).toBe('logsource:\n product: windows\n category: file_event\ndetection:\n selection:\n TargetFilename|contains:\n - ds7002.lnk\n - ds7002.pdf\n - ds7002.zip\n condition: selection'); expect(comp.extractedCreated).toBe('2018/11/20'); expect(comp.extractedUpdated).toBe('2023/02/20'); - expect(comp.extractedAuthor).toBe('@41thexplorer'); }); test('fixProtocol', () => { @@ -275,7 +272,7 @@ test('canAddOverride elastalert', () => { comp.detect.overrides = [ { - type: 'custom filter', + type: 'customFilter', isEnabled: 'isEnabled', createdAt: 'createdAt', updatedAt: 'updatedAt', @@ -290,7 +287,7 @@ test('canAddOverride elastalert', () => { }, ]; - expect(comp.canAddOverride()).toBe(false); + expect(comp.canAddOverride()).toBe(true); }); test('tagOverrides', () => { @@ -305,6 +302,104 @@ test('tagOverrides', () => { comp.tagOverrides(); for (let i = 0; i < comp.detect.overrides.length; i++) { - expect(comp.detect.overrides[0]).toStrictEqual({ index: 0 }); + expect(comp.detect.overrides[i]).toStrictEqual({ index: i }); } -}); \ No newline at end of file +}); + +test('deleteDetection', async () => { + const mock = jest.fn().mockReturnValue(Promise.resolve({ data: [] })); + const showErrorMock = mockShowError(); + comp.$root.papi['delete'] = mock; + comp.$route.params.id = "testid" + await comp.confirmDeleteDetection(); + expect(comp.confirmDeleteDialog).toBe(false); + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith('/detection/testid'); + expect(comp.$root.loading).toBe(false); + expect(showErrorMock).toHaveBeenCalledTimes(0); + expect(comp.$router.length).toBe(1); +}); + +test('deleteDetectionCancel', () => { + expect(comp.confirmDeleteDialog).toBe(false); + comp.deleteDetection(); + expect(comp.confirmDeleteDialog).toBe(true); + comp.cancelDeleteDetection(); + expect(comp.confirmDeleteDialog).toBe(false); + comp.deleteDetection(); +}) + +test('deleteDetectionFailure', async () => { + resetPapi().mockPapi("delete", null, new Error("something bad")); + const showErrorMock = mockShowError(); + comp.$root.papi['delete'] = mock; + comp.$route.params.id = "testid" + comp.deleteDetection(); + await comp.confirmDeleteDetection(); + expect(comp.confirmDeleteDialog).toBe(false); + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith('/detection/testid'); + expect(comp.$root.loading).toBe(false); + expect(showErrorMock).toHaveBeenCalledTimes(1); + expect(comp.$router.length).toBe(0); +}); + +test('isDetectionSourceDirty', () => { + comp.detect = { + content: 'X', + }; + comp.origDetect = Object.assign({}, comp.detect); + + expect(comp.isDetectionSourceDirty()).toBe(false); + + comp.detect.content = 'Y'; + + expect(comp.isDetectionSourceDirty()).toBe(true); + + comp.origDetect.content = 'Y'; + + expect(comp.isDetectionSourceDirty()).toBe(false); +}); + +test('revertEnabled', () => { + comp.detect = { + isEnabled: true, + }; + comp.origDetect = Object.assign({}, comp.detect); + + // both true + comp.revertEnabled(); + expect(comp.detect.isEnabled).toBe(true); + expect(comp.origDetect.isEnabled).toBe(true); + + // det false, orig true + comp.detect.isEnabled = false; + comp.revertEnabled(); + expect(comp.detect.isEnabled).toBe(true); + expect(comp.origDetect.isEnabled).toBe(true); + + // det true, orig false + comp.detect.isEnabled = true; + comp.origDetect.isEnabled = false; + comp.revertEnabled(); + expect(comp.detect.isEnabled).toBe(false); + expect(comp.origDetect.isEnabled).toBe(false); + + // both false + comp.revertEnabled(); + expect(comp.detect.isEnabled).toBe(false); + expect(comp.origDetect.isEnabled).toBe(false); +}) + +test('isFieldValid', () => { + comp.$refs = {} + expect(comp.isFieldValid('foo')).toBe(true) + + comp.$refs = {bar: { valid: false}} + expect(comp.isFieldValid('foo')).toBe(true) + expect(comp.isFieldValid('bar')).toBe(false) + + comp.$refs = {bar: { valid: true}} + expect(comp.isFieldValid('bar')).toBe(true) + +}) \ No newline at end of file diff --git a/html/js/routes/hunt.js b/html/js/routes/hunt.js index 300127256..2804ad7af 100644 --- a/html/js/routes/hunt.js +++ b/html/js/routes/hunt.js @@ -148,6 +148,8 @@ const huntComponent = { { text: this.$root.i18n.disable, value: 'disable' }, ], quickActionDetId: null, + presets: {}, + manualSyncTargetEngine: null, }}, created() { this.$root.initializeCharts(); @@ -245,10 +247,12 @@ const huntComponent = { this.chartLabelMaxLength = params["chartLabelMaxLength"] this.chartLabelOtherLimit = params["chartLabelOtherLimit"] this.chartLabelFieldSeparator = params["chartLabelFieldSeparator"] + this.presets = params["presets"]; + this.manualSyncTargetEngine = this.getPresets("manualSync")[0]; if (this.queries != null && this.queries.length > 0) { this.query = this.queries[0].query; } - this.actions = params["actions"]; + this.actions = params["actions"] || []; this.zone = moment.tz.guess(); this.loadLocalSettings(); @@ -436,17 +440,6 @@ const huntComponent = { this.autoRefreshEnabled = false; this.autoRefreshInterval = 0; } - - // Check for special params that force a re-route - var reRoute = false; - if (this.$route.query.filterValue) { - this.filterQuery(this.$route.query.filterField, this.$route.query.filterValue, this.$route.query.filterMode, undefined, this.$route.query.scalar === 'true'); - reRoute = true; - } - if (this.$route.query.groupByField) { - this.groupQuery(this.$route.query.groupByField, this.$route.query.groupByGroup); - reRoute = true; - } if (Array.isArray(this.filterToggles)) { for (const q in this.$route.query) { this.filterToggles.forEach(toggle => { @@ -457,14 +450,24 @@ const huntComponent = { enabled = enabled.toLowerCase() === 'true'; } toggle.enabled = enabled; - if (orig !== toggle.enabled) { - reRoute = true; - } } }); } } + + // Check for special params that force a re-route. This is needed when async functions will handle the hunt themselves. + // So setting reroute=true tells the current thread not to perform the hunt, because the async thread will be doing it momentarily. + var reRoute = false; + if (this.$route.query.filterValue) { + this.filterQuery(this.$route.query.filterField, this.$route.query.filterValue, this.$route.query.filterMode, undefined, this.$route.query.scalar === 'true'); + reRoute = true; + } + if (this.$route.query.groupByField) { + this.groupQuery(this.$route.query.groupByField, this.$route.query.groupByGroup); + reRoute = true; + } if (reRoute) return false; + return true; }, async loadData() { @@ -479,78 +482,20 @@ const huntComponent = { // This must occur before the following await, so that Vue flushes the old groupby DOM renders this.groupBys.splice(0); - let response; - if (this.category === 'playbooks') { - response = { - data: { - "metrics": { - "timeline": null, - }, - "elapsedMs": 668, - "errors": [], - "criteria": { - "query": "(_id:*) AND _index:\"*:so-case\" AND so_kind:detection", - "dateRange": "", - "metricLimit": 0, - "eventLimit": 50, - "BeginTime": "2021-08-23T15:41:39-06:00", - "EndTime": "2023-08-23T15:41:39-06:00", - "CreateTime": "2023-08-23T15:41:39.446264196-06:00", - "ParsedQuery": { - "Segments": [ - {} - ] - }, - "SortFields": null - }, - "events": [ - { - "source": "manager:so-case", - "Time": "2023-08-23T14:48:17.140438075-06:00", - "timestamp": "2023-08-23T14:48:17.140Z", - "id": "RDmUHooB-8rNCo4d3nIc", - "type": "", - "score": 3.287682, - "payload": { - "@timestamp": "2023-08-23T14:48:17.140438075-06:00", - "so_playbook.onionId": "75332a3c-b029-46a0-9392-509ff90737a8", - "so_playbook.publicId": "4020131e-223a-421e-8ebe-8a211a5ac4d6", - "so_playbook.title": "Find the baddies", - "so_playbook.severity": "high", - "so_playbook.description": "A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines.", - "so_playbook.mechanism": "suricata", - "so_playbook.tags": ["one", "two", "three"], - "so_playbook.relatedPlaybooks": [], - "so_playbook.contributors": ["Jim Bob"], - "so_playbook.userEditable": true, - "so_playbook.createTime": "2023-08-22T12:49:47.302819008-06:00", - "so_playbook.kind": "playbook", - "so_playbook.userId": "83656890-2acd-4c0b-8ab9-7c73e71ddaf3", - "so_kind": "playbook" - } - } - ], - createTime: moment().subtract(2, 'seconds').toISOString(), - completeTime: moment().toISOString(), - } - }; - response.data.totalEvents = response.data.events.length; - } else { - let range = this.dateRange; - if (this.isCategory('detections')) { - range = moment(0).format(this.i18n.timePickerFormat) + " - " + moment().format(this.i18n.timePickerFormat); + let range = this.dateRange; + if (this.isCategory('detections')) { + range = moment(0).format(this.i18n.timePickerFormat) + " - " + moment().format(this.i18n.timePickerFormat); + } + let response = await this.$root.papi.get('events/', { + params: { + query: await this.getQuery(), + range: range, + format: this.i18n.timePickerSample, + zone: this.zone, + metricLimit: this.groupByLimit, + eventLimit: this.eventLimit } - response = await this.$root.papi.get('events/', { - params: { - query: await this.getQuery(), - range: range, - format: this.i18n.timePickerSample, - zone: this.zone, - metricLimit: this.groupByLimit, - eventLimit: this.eventLimit - } - }); - } + }); this.eventPage = 1; this.groupByPage = 1; @@ -578,6 +523,13 @@ const huntComponent = { } this.$root.stopLoading(); }, + getPresets(kind) { + if (this.presets && this.presets[kind]) { + return this.presets[kind].labels; + } + + return []; + }, async filterQuery(field, value, filterMode, notify = true, scalar = false) { try { const valueType = typeof value; @@ -1040,7 +992,7 @@ const huntComponent = { return this.buildGroupOptionRoute(groupIdx, removals, ''); }, countDrilldown(event) { - if ( (Object.keys(event).length == 2 && Object.keys(event)[0] == "count") || (Object.keys(event).length == 4 && Object.keys(event)[0] == "count" && Object.keys(event)[1] == "rule.name" && Object.keys(event)[2] == "event.module" && Object.keys(event)[3] == "event.severity_label") ) { + if ( (Object.keys(event).length == 2 && Object.keys(event)[0] == "count") || (Object.keys(event).length == 5 && Object.keys(event)[0] == "count" && Object.keys(event)[1] == "rule.name" && Object.keys(event)[2] == "event.module" && Object.keys(event)[3] == "event.severity_label" && Object.keys(event)[4] == "rule.uuid") ) { this.filterRouteDrilldown = this.buildFilterRoute(Object.keys(event)[1], event[Object.keys(event)[1]], FILTER_DRILLDOWN); this.$router.push(this.filterRouteDrilldown); } @@ -1072,19 +1024,12 @@ const huntComponent = { } if (this.isCategory('alerts')) { - const alert = this.eventData.find(item => { - for (const key in event) { - if (key !== "count" && item[key] !== event[key]) { - return false; - } - } - return true; - }); + const id = event["rule.uuid"]; + this.quickActionDetId = null; - if (alert) { - // don't slow down the UI with this call - const publicId = alert["rule.uuid"]; - this.$root.papi.get(`detection/public/${publicId}`).then(response => { + // don't slow down the UI with this call + if (id) { + this.$root.papi.get(`detection/public/${id}`).then(response => { this.quickActionDetId = response.data.id; }); } @@ -2307,6 +2252,25 @@ const huntComponent = { this.$root.showInfo(msg); } }, + startManualSync(engine, type) { + if (!engine) { + this.$root.showTip(this.i18n.engineSelect); + return; + } + try { + this.$root.papi.post(`detection/sync/${engine}/${type}`); + + let msg = this.i18n.startSyncFull; + if (type !== 'full') { + msg = this.i18n.startSyncUpdate; + } + + msg = msg.replace("{engine}", engine); + this.$root.showTip(msg); + } catch (e) { + this.$root.showError(e); + } + }, } }; @@ -2323,6 +2287,3 @@ routes.push({ path: '/dashboards', name: 'dashboards', component: dashboardsComp const detectionsComponent = Object.assign({}, huntComponent); routes.push({ path: '/detections', name: 'detections', component: detectionsComponent }); - -const playbooksComponent = Object.assign({}, huntComponent); -routes.push({ path: '/playbooks', name: 'playbooks', component: playbooksComponent }); diff --git a/html/js/routes/playbook.js b/html/js/routes/playbook.js deleted file mode 100644 index 3db5a5f02..000000000 --- a/html/js/routes/playbook.js +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright 2019 Jason Ertel (github.com/jertel). -// Copyright 2020-2023 Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one -// or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at -// https://securityonion.net/license; you may not use this file except in compliance with the -// Elastic License 2.0. - -routes.push({ path: '/playbook/:id', name: 'playbook', component: { - template: '#page-playbook', - data() { return { - i18n: this.$root.i18n, - params: {}, - rules: { - required: value => (value && value.length > 0) || this.$root.i18n.required, - number: value => (! isNaN(+value) && Number.isInteger(parseFloat(value))) || this.$root.i18n.required, - hours: value => (!value || /^\d{1,4}(\.\d{1,4})?$/.test(value)) || this.$root.i18n.invalidHours, - shortLengthLimit: value => (value.length < 100) || this.$root.i18n.required, - longLengthLimit: value => (encodeURI(value).split(/%..|./).length - 1 < 10000000) || this.$root.i18n.required, - fileSizeLimit: value => (value == null || value.size < this.maxUploadSizeBytes) || this.$root.i18n.fileTooLarge.replace("{maxUploadSizeBytes}", this.$root.formatCount(this.maxUploadSizeBytes)), - fileNotEmpty: value => (value == null || value.size > 0) || this.$root.i18n.fileEmpty, - fileRequired: value => (value != null) || this.$root.i18n.required, - }, - playbook: null, - origPlaybook: null, - curEditTarget: null, // string containing element ID, null if not editing - origValue: null, - editField: null, - severityOptions: ['low', 'medium', 'high'], - engineOptions: ['none', 'suricata', 'yara', 'elastalert'], - panel: [0, 1, 2], - activeTab: '', - tags: [], - addQuestion: '', - addContext: '', - addDataSources: [], - addQuery: '', - search: '', - questionHeaders: [{ title: '@timestamp', key: 'item.payload["@timestamp"]' }, { title: '@version', value: '@version' }, { title: 'message', value: 'message' }], - }}, - created() { - }, - watch: { - }, - mounted() { - this.$root.loadParameters('playbooks', this.initPlaybook); - }, - methods: { - async initPlaybook(params) { - this.params = params; - if (this.$route.params.id === 'create') { - this.detect = this.newPlaybook(); - } else { - await this.loadData(); - } - - this.origPlaybook = Object.assign({}, this.playbook); - - this.loadUrlParameters(); - }, - loadUrlParameters() { - - }, - newPlaybook() { - let author = [this.$root.user.firstName, this.$root.user.lastName].filter(x => x).join(' '); - return { - publicId: '', - title: 'Detection title not yet provided - click here to update this title', - description: 'Detection description not yet provided - click here to update this description', - mechanism: '', - tags: [], - relatedPlaybooks: [], - detectionLinks: [], - contributors: [author], - userEditable: true, - questions: [], - note: '', - } - }, - async loadData() { - this.$root.startLoading(); - - // try { - // const response = await this.$root.papi.get('playbook/' + encodeURIComponent(this.$route.params.id)); - // this.detect = response.data; - // } catch (error) { - // if (error.response != undefined && error.response.status == 404) { - // this.$root.showError(this.i18n.notFound); - // } else { - // this.$root.showError(error); - // } - // } - if (this.isNew()) { - this.playbook = this.newPlaybook(); - } else { - this.playbook = { - onionId: this.$route.params.id, - publicId: "4020131e-223a-421e-8ebe-8a211a5ac4d6", - title: "Find the baddies", - severity: "high", - description: "A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines.", - mechanism: "suricata", - tags: ["one", "two", "three"], - relatedPlaybooks: [], - contributors: ["Corey Ogburn"], - userEditable: true, - createTime: "2023-08-22T12:49:47.302819008-06:00", - userId: "83656890-2acd-4c0b-8ab9-7c73e71ddaf3", - questions: [ - { - question: "What network resources did they access?", - context: "Bad actors do bad things to network resources", - dataSources: ['windows_security', 'process_auditing'], - query: '*', - results: [ - { - "source": "manager:.ds-logs-elastic_agent-default-2023.08.21-000001", - "Time": "2023-08-24T15:10:05.747Z", - "timestamp": "2023-08-24T15:10:05.747Z", - "id": "y5IYKIoB9-Z7uL2kmy_o", - "type": "", - "score": 2, - "payload": { - "@timestamp": "2023-08-24T15:10:05.747Z", - "@version": "1", - "agent.ephemeral_id": "b36260f3-7e22-4ada-9736-7825bba61050", - "agent.id": "6a7d0533-85fe-4aab-ad23-25659d59415f", - "agent.name": "manager", - "agent.type": "filebeat", - "agent.version": "8.8.2", - "container.id": "elastic-agent-cdc5ba", - "data_stream.dataset": "elastic_agent", - "data_stream.namespace": "default", - "data_stream.type": "logs", - "ecs.version": "8.0.0", - "elastic_agent.id": "6a7d0533-85fe-4aab-ad23-25659d59415f", - "elastic_agent.snapshot": false, - "elastic_agent.version": "8.8.2", - "event.agent_id_status": "auth_metadata_missing", - "event.dataset": "elastic_agent", - "event.ingested": "2023-08-24T15:10:16Z", - "event.module": "elastic_agent", - "host.architecture": "x86_64", - "host.containerized": false, - "host.hostname": "manager", - "host.id": "9b909224852841ee9e8394c0ccb6f345", - "host.ip": [ - "192.168.122.119", - "172.17.1.1", - "172.17.0.1" - ], - "host.mac": [ - "02-42-F7-90-7C-0F", - "02-42-FD-01-4C-18", - "22-69-52-DA-7E-60", - "26-32-CC-C2-D0-96", - "32-5D-A3-D9-5E-BB", - "36-30-04-3E-2A-74", - "3A-47-4A-42-2D-42", - "52-54-00-17-A5-16", - "52-54-00-79-9B-AF", - "56-22-21-D2-59-BB", - "5A-84-1E-33-78-6F", - "5A-B1-C1-9E-26-54", - "5E-07-4D-47-FC-B6", - "66-FE-30-61-E6-77", - "82-20-8F-6D-BA-2B", - "86-14-06-40-98-7D", - "86-29-18-6F-10-88", - "8E-AD-CA-9F-EA-F6", - "96-4D-FB-AD-AB-8E", - "AA-FB-73-2B-C4-24", - "B2-68-3A-55-50-88", - "BE-53-9A-99-67-0C", - "CA-68-C4-C1-5B-4D", - "E2-0C-CE-E7-DA-50", - "EE-F2-CD-8D-0A-17", - "FE-99-5F-39-DF-D1" - ], - "host.name": "manager", - "host.os.family": "redhat", - "host.os.kernel": "5.15.0-103.114.4.el9uek.x86_64", - "host.os.name": "Oracle Linux Server", - "host.os.platform": "ol", - "host.os.type": "linux", - "host.os.version": "9.2", - "input.type": "filestream", - "log.file.path": "/opt/Elastic/Agent/data/elastic-agent-cdc5ba/logs/elastic-agent-20230824.ndjson", - "log.level": "info", - "log.offset": 311104, - "log.origin.file.line": 821, - "log.origin.file.name": "coordinator/coordinator.go", - "log.source": "elastic-agent", - "message": "Updating running component model", - "metadata.beat": "filebeat", - "metadata.input.beats.host.ip": "172.17.1.1", - "metadata.input_id": "filestream-monitoring-agent", - "metadata.raw_index": "logs-elastic_agent-default", - "metadata.stream_id": "filestream-monitoring-agent", - "metadata.type": "_doc", - "metadata.version": "8.8.2", - "tags": [ - "elastic-agent", - "input-manager", - "beats_input_codec_plain_applied" - ] - } - }, - { - "source": "manager:.ds-logs-elastic_agent-default-2023.08.21-000001", - "Time": "2023-08-24T15:09:25.702Z", - "timestamp": "2023-08-24T15:09:25.702Z", - "id": "GJIXKIoB9-Z7uL2k7C-E", - "type": "", - "score": 2, - "payload": { - "@timestamp": "2023-08-24T15:09:25.702Z", - "@version": "1", - "agent.ephemeral_id": "b36260f3-7e22-4ada-9736-7825bba61050", - "agent.id": "6a7d0533-85fe-4aab-ad23-25659d59415f", - "agent.name": "manager", - "agent.type": "filebeat", - "agent.version": "8.8.2", - "container.id": "elastic-agent-cdc5ba", - "data_stream.dataset": "elastic_agent", - "data_stream.namespace": "default", - "data_stream.type": "logs", - "ecs.version": "8.0.0", - "elastic_agent.id": "6a7d0533-85fe-4aab-ad23-25659d59415f", - "elastic_agent.snapshot": false, - "elastic_agent.version": "8.8.2", - "event.agent_id_status": "auth_metadata_missing", - "event.dataset": "elastic_agent", - "event.ingested": "2023-08-24T15:09:31Z", - "event.module": "elastic_agent", - "host.architecture": "x86_64", - "host.containerized": false, - "host.hostname": "manager", - "host.id": "9b909224852841ee9e8394c0ccb6f345", - "host.ip": [ - "192.168.122.119", - "172.17.1.1", - "172.17.0.1" - ], - "host.mac": [ - "02-42-F7-90-7C-0F", - "02-42-FD-01-4C-18", - "22-69-52-DA-7E-60", - "26-32-CC-C2-D0-96", - "32-5D-A3-D9-5E-BB", - "36-30-04-3E-2A-74", - "3A-47-4A-42-2D-42", - "52-54-00-17-A5-16", - "52-54-00-79-9B-AF", - "56-22-21-D2-59-BB", - "5A-84-1E-33-78-6F", - "5A-B1-C1-9E-26-54", - "5E-07-4D-47-FC-B6", - "66-FE-30-61-E6-77", - "82-20-8F-6D-BA-2B", - "86-14-06-40-98-7D", - "86-29-18-6F-10-88", - "8E-AD-CA-9F-EA-F6", - "96-4D-FB-AD-AB-8E", - "AA-FB-73-2B-C4-24", - "AE-49-EF-7B-5E-B9", - "B2-68-3A-55-50-88", - "BE-53-9A-99-67-0C", - "CA-68-C4-C1-5B-4D", - "E2-0C-CE-E7-DA-50", - "EE-F2-CD-8D-0A-17", - "FE-99-5F-39-DF-D1" - ], - "host.name": "manager", - "host.os.family": "redhat", - "host.os.kernel": "5.15.0-103.114.4.el9uek.x86_64", - "host.os.name": "Oracle Linux Server", - "host.os.platform": "ol", - "host.os.type": "linux", - "host.os.version": "9.2", - "input.type": "filestream", - "log.file.path": "/opt/Elastic/Agent/data/elastic-agent-cdc5ba/logs/elastic-agent-20230824.ndjson", - "log.level": "error", - "log.offset": 304406, - "log.origin.file.line": 221, - "log.origin.file.name": "fleet/fleet_gateway.go", - "log.source": "elastic-agent", - "message": "Checkin request to fleet-server succeeded after 2 failures", - "metadata.beat": "filebeat", - "metadata.input.beats.host.ip": "172.17.1.1", - "metadata.input_id": "filestream-monitoring-agent", - "metadata.raw_index": "logs-elastic_agent-default", - "metadata.stream_id": "filestream-monitoring-agent", - "metadata.type": "_doc", - "metadata.version": "8.8.2", - "tags": [ - "elastic-agent", - "input-manager", - "beats_input_codec_plain_applied" - ] - } - }, - { - "source": "manager:.ds-logs-elastic_agent-default-2023.08.21-000001", - "Time": "2023-08-24T15:08:46.446Z", - "timestamp": "2023-08-24T15:08:46.446Z", - "id": "lZIXKIoB9-Z7uL2kTy6x", - "type": "", - "score": 2, - "payload": { - "@timestamp": "2023-08-24T15:08:46.446Z", - "@version": "1", - "agent.ephemeral_id": "b36260f3-7e22-4ada-9736-7825bba61050", - "agent.id": "6a7d0533-85fe-4aab-ad23-25659d59415f", - "agent.name": "manager", - "agent.type": "filebeat", - "agent.version": "8.8.2", - "container.id": "elastic-agent-cdc5ba", - "data_stream.dataset": "elastic_agent", - "data_stream.namespace": "default", - "data_stream.type": "logs", - "ecs.version": "8.0.0", - "elastic_agent.id": "6a7d0533-85fe-4aab-ad23-25659d59415f", - "elastic_agent.snapshot": false, - "elastic_agent.version": "8.8.2", - "event.agent_id_status": "auth_metadata_missing", - "event.dataset": "elastic_agent", - "event.ingested": "2023-08-24T15:08:50Z", - "event.module": "elastic_agent", - "host.architecture": "x86_64", - "host.containerized": false, - "host.hostname": "manager", - "host.id": "9b909224852841ee9e8394c0ccb6f345", - "host.ip": [ - "192.168.122.119", - "172.17.1.1", - "172.17.0.1" - ], - "host.mac": [ - "02-42-F7-90-7C-0F", - "02-42-FD-01-4C-18", - "22-69-52-DA-7E-60", - "26-32-CC-C2-D0-96", - "32-5D-A3-D9-5E-BB", - "36-30-04-3E-2A-74", - "3A-47-4A-42-2D-42", - "52-54-00-17-A5-16", - "52-54-00-79-9B-AF", - "56-22-21-D2-59-BB", - "5A-84-1E-33-78-6F", - "5A-B1-C1-9E-26-54", - "5E-07-4D-47-FC-B6", - "66-FE-30-61-E6-77", - "82-20-8F-6D-BA-2B", - "86-14-06-40-98-7D", - "86-29-18-6F-10-88", - "8E-AD-CA-9F-EA-F6", - "96-4D-FB-AD-AB-8E", - "AA-FB-73-2B-C4-24", - "AE-49-EF-7B-5E-B9", - "B2-68-3A-55-50-88", - "BE-53-9A-99-67-0C", - "CA-68-C4-C1-5B-4D", - "E2-0C-CE-E7-DA-50", - "EE-F2-CD-8D-0A-17", - "FE-99-5F-39-DF-D1" - ], - "host.name": "manager", - "host.os.family": "redhat", - "host.os.kernel": "5.15.0-103.114.4.el9uek.x86_64", - "host.os.name": "Oracle Linux Server", - "host.os.platform": "ol", - "host.os.type": "linux", - "host.os.version": "9.2", - "input.type": "filestream", - "log.file.path": "/opt/Elastic/Agent/data/elastic-agent-cdc5ba/logs/elastic-agent-20230824.ndjson", - "log.level": "info", - "log.offset": 296546, - "log.origin.file.line": 821, - "log.origin.file.name": "coordinator/coordinator.go", - "log.source": "elastic-agent", - "message": "Updating running component model", - "metadata.beat": "filebeat", - "metadata.input.beats.host.ip": "172.17.1.1", - "metadata.input_id": "filestream-monitoring-agent", - "metadata.raw_index": "logs-elastic_agent-default", - "metadata.stream_id": "filestream-monitoring-agent", - "metadata.type": "_doc", - "metadata.version": "8.8.2", - "tags": [ - "elastic-agent", - "input-manager", - "beats_input_codec_plain_applied" - ] - } - }, - ] - } - ], - note: "This is a note", - }; - } - - this.$root.stopLoading(); - }, - isNew() { - return this.$route.params.id === 'create'; - }, - cancelPlaybook() { - if (this.isNew()) { - this.$router.push({name: 'playbooks'}); - } else { - this.playbook = this.origPlaybook; - } - }, - generatePublicID() { - this.playbook.publicId = crypto.randomUUID(); - }, - async startEdit(target, field) { - if (this.curEditTarget === target) return; - if (this.curEditTarget !== null) await this.stopEdit(false); - - this.curEditTarget = target; - this.origValue = this.playbook[field]; - this.editField = field; - - this.$nextTick(() => { - const el = document.getElementById(target + '-edit'); - if (el) { - el.focus(); - el.select(); - } - }); - }, - isEdit(target) { - return this.curEditTarget === target; - }, - async stopEdit(commit) { - if (!commit) { - this.playbook[this.editField] = this.origValue; - } else if (!this.isNew()) { - // const response = await this.$root.papi.put('/playbook', this.playbook); - // console.log('UPDATE', response); - } - - this.curEditTarget = null; - this.origValue = null; - this.editField = null; - }, - savePlaybook(createNew) { - // if (createNew) { - // this.$root.papi.post('/playbook', this.playbook); - // } else { - // this.$root.papi.put('/playbook', this.playbook); - // } - - this.origPlaybook = Object.assign({}, this.playbook); - }, - print(x) { - console.log(x); - }, - } -}}); diff --git a/html/login/index.html b/html/login/index.html index 19f26bbf3..42543aef9 100644 --- a/html/login/index.html +++ b/html/login/index.html @@ -19,38 +19,39 @@ + Security Onion -