├── .eslintrc.js ├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── parseRuleData.ts ├── public └── images │ ├── 404_rainy_cloud_dark.png │ ├── 404_rainy_cloud_light.png │ ├── favicon │ ├── dev │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon-96x96.png │ └── prod │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon-96x96.png │ ├── home │ ├── illustration-eui-hero-500-darkmode-shadow.svg │ └── illustration-eui-hero-500-shadow.svg │ ├── logo-eui.svg │ └── patterns │ ├── pattern-1.svg │ ├── pattern-2-dark.svg │ ├── pattern-2-light.svg │ └── pattern-2.svg ├── src ├── components │ ├── chrome │ │ ├── index.tsx │ │ └── theme_switcher.tsx │ ├── details │ │ └── rule_details.styles.ts │ ├── home │ │ ├── gradient_bg.styles.ts │ │ ├── gradient_bg.tsx │ │ ├── header.styles.ts │ │ ├── header.tsx │ │ ├── home_hero.styles.ts │ │ ├── home_hero.tsx │ │ ├── paste.txt │ │ ├── rule_filter.styles.ts │ │ ├── rule_filter.tsx │ │ ├── rule_list.styles.ts │ │ ├── rule_list.tsx │ │ ├── rule_panel.styles.ts │ │ ├── rule_panel.tsx │ │ ├── theme_switcher.styles.ts │ │ ├── theme_switcher.tsx │ │ ├── wrapper.styles.ts │ │ └── wrapper.tsx │ ├── next_eui │ │ └── button.tsx │ └── theme.tsx ├── custom_typings │ └── index.d.ts ├── images │ └── logo_elastic.png ├── lib │ ├── gtag.ts │ ├── loader.ts │ ├── ruledata.ts │ └── theme.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ ├── index.tsx │ └── rules │ │ └── [id].tsx ├── styles │ ├── getting-started.styles.ts │ └── global.styles.ts └── types │ └── index.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@typescript-eslint/recommended', 4 | 'prettier', 5 | 'next/core-web-vitals', 6 | ], 7 | plugins: ['prettier' 8 | ], 9 | rules: { 10 | // In an ideal world, we'd never have to use @ts-ignore, but that's not 11 | // possible right now. 12 | '@typescript-eslint/ban-ts-ignore': 'off', 13 | '@typescript-eslint/ban-ts-comment': 'off', 14 | // Again, in theory this is a good rule, but it can cause a bit of 15 | // unhelpful noise. 16 | '@typescript-eslint/explicit-function-return-type': 'off', 17 | // Another theoretically good rule, but sometimes we know better than 18 | // the linter. 19 | '@typescript-eslint/no-non-null-assertion': 'off', 20 | // Accessibility is important to EUI. Enforce all a11y rules. 21 | 'jsx-a11y/accessible-emoji': 'error', 22 | 'jsx-a11y/alt-text': 'error', 23 | 'jsx-a11y/anchor-has-content': 'error', 24 | 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', 25 | 'jsx-a11y/aria-props': 'error', 26 | 'jsx-a11y/aria-proptypes': 'error', 27 | 'jsx-a11y/aria-role': 'error', 28 | 'jsx-a11y/aria-unsupported-elements': 'error', 29 | 'jsx-a11y/heading-has-content': 'error', 30 | 'jsx-a11y/html-has-lang': 'error', 31 | 'jsx-a11y/iframe-has-title': 'error', 32 | 'jsx-a11y/interactive-supports-focus': 'error', 33 | 'jsx-a11y/media-has-caption': 'error', 34 | 'jsx-a11y/mouse-events-have-key-events': 'error', 35 | 'jsx-a11y/no-access-key': 'error', 36 | 'jsx-a11y/no-distracting-elements': 'error', 37 | 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error', 38 | 'jsx-a11y/no-noninteractive-element-interactions': 'error', 39 | 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error', 40 | 'jsx-a11y/no-redundant-roles': 'error', 41 | 'jsx-a11y/role-has-required-aria-props': 'error', 42 | 'jsx-a11y/role-supports-aria-props': 'error', 43 | 'jsx-a11y/scope': 'error', 44 | 'jsx-a11y/tabindex-no-positive': 'error', 45 | 'jsx-a11y/label-has-associated-control': 'error', 46 | 47 | 'react-hooks/rules-of-hooks': 'error', 48 | 'react-hooks/exhaustive-deps': 'warn', 49 | 50 | "react/no-unknown-property": ["error", { "ignore": ["css"] }], 51 | 52 | 'prefer-object-spread': 'error', 53 | // Use template strings instead of string concatenation 54 | 'prefer-template': 'error', 55 | // This is documented as the default, but apparently now needs to be 56 | // set explicitly 57 | 'prettier/prettier': [ 58 | 'error', 59 | {}, 60 | { 61 | usePrettierrc: true, 62 | }, 63 | ], 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: GitHub Pages deploy 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | schedule: 10 | # update the site with newest rules once per day 11 | - cron: '15 0 * * *' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 🛎️ 19 | uses: actions/checkout@v2.3.1 20 | - name: Use Node.js 18.16.1 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: '18.16.1' 24 | 25 | - name: Installing my packages 26 | run: npm ci 27 | 28 | - name: Build my App 29 | env: 30 | PATH_PREFIX: true 31 | run: npm run build && npm run export && touch ./out/.nojekyll 32 | 33 | - name: Deploy 🚀 34 | uses: JamesIves/github-pages-deploy-action@v4.4.1 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | BRANCH: gh-pages # The branch the action should deploy to. 38 | FOLDER: out # The folder the action should deploy. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Junk and IDE stuff 2 | *.bak 3 | *.iml 4 | *.orig 5 | *.rej 6 | *.swp 7 | *~ 8 | .DS_Store 9 | .idea 10 | .vim/.netrwhist 11 | yarn-error.log 12 | .eslintcache 13 | 14 | # Files in here are copied by the build 15 | public/themes 16 | 17 | node_modules 18 | .next 19 | 20 | # Default `next export` output directory 21 | out 22 | 23 | # TypeScript cache 24 | tsconfig.tsbuildinfo 25 | 26 | #Bring back lib that is ignored at the top-level 27 | !lib 28 | 29 | #calculated rule data 30 | src/data/** 31 | parseRuleData.js 32 | ~ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true, 3 | "jsxSingleQuote": false, 4 | "parser": "typescript", 5 | "printWidth": 80, 6 | "semi": true, 7 | "singleQuote": true, 8 | "trailingComma": "es5", 9 | "arrowParens": "avoid" 10 | } 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 elastic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Detection Rules Explorer 2 | 3 | A UI for exploring and learning about Elastic Security Detection Rules. 4 | 5 | ## How do I get to the site? 6 | 7 | The explorer is publically available at https://elastic.github.io/detection-rules-explorer. It is updated daily with the latest published rules. 8 | 9 | ## What rules are included? 10 | 11 | Elastic detection rules are included from these Elastic packages: 12 | 13 | - [Prebuilt Security Detection Rules](https://github.com/elastic/detection-rules/tree/main) 14 | - [Domain Generated Algorithm Detection](https://github.com/elastic/integrations/tree/main/packages/dga) 15 | - [Living off the Land Attack Detection](https://github.com/elastic/integrations/tree/main/packages/problemchild) 16 | - [Lateral Movement Detection](https://github.com/elastic/integrations/tree/main/packages/lmd) 17 | - [Data Exfiltration Detection](https://github.com/elastic/integrations/tree/main/packages/ded) 18 | 19 | ## How do I getting started with development? 20 | 21 | The site is built with GitHub Pages, Next.js and Elastic EUI, based on the [Elastic's Next.js EUI Starter](https://github.com/elastic/next-eui-starter). 22 | 23 | To run the local development environment: 24 | 25 | 1. Get going with node: 26 | ```bash 27 | nvm use 28 | ``` 29 | 30 | 2. Get the latest rules: 31 | 32 | ```bash 33 | npm run prebuild 34 | ``` 35 | 36 | 1. Start the development server: 37 | 38 | ```bash 39 | npm run dev 40 | ``` 41 | 42 | From there, open [http://localhost:3000](http://localhost:3000) with your browser to see the result. It will hot reload as you make changes to the site code. 43 | 44 | ## How does this get deployed to Github pages? 45 | 46 | There are two branches in this repository: 47 | 48 | - `main` - stores the source code for the site 49 | - `gh-pages` - stores the compiled site source for publishing 50 | 51 | On merge to `main`, a Github action (at `.github/workflows/gh-pages.yml`) will build the site and push it to the `gh-pages` branch. From there, another Github action (auto-configured by Github) will publish the updates to the internet at https://elastic.github.io/detection-rules-explorer. 52 | 53 | ## Learn More 54 | 55 | To learn more about Next.js, take a look at the following resources: 56 | 57 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 58 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 59 | - [Elastic Next.js Starter](https://github.com/elastic/next-eui-starter) - on which this repo was originally based. 60 | - [Elastic EUI Documentation](https://eui.elastic.co/) - Elastic's react component library. 61 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires,@typescript-eslint/no-use-before-define,@typescript-eslint/no-empty-function,prefer-template */ 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const glob = require('glob'); 5 | const path = require('path'); 6 | const iniparser = require('iniparser'); 7 | 8 | const withBundleAnalyzer = require('@next/bundle-analyzer'); 9 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 10 | const { IgnorePlugin } = require('webpack'); 11 | 12 | /** 13 | * If you are deploying your site under a directory other than `/` e.g. 14 | * GitHub pages, then you have to tell Next where the files will be served. 15 | * We don't need this during local development, because everything is 16 | * available under `/`. 17 | */ 18 | const usePathPrefix = process.env.PATH_PREFIX === 'true'; 19 | 20 | const pathPrefix = usePathPrefix ? derivePathPrefix() : ''; 21 | 22 | const themeConfig = buildThemeConfig(); 23 | 24 | const nextConfig = { 25 | compiler: { 26 | emotion: true, 27 | }, 28 | /** Disable the `X-Powered-By: Next.js` response header. */ 29 | poweredByHeader: false, 30 | 31 | /** 32 | * When set to something other than '', this field instructs Next to 33 | * expect all paths to have a specific directory prefix. This fact is 34 | * transparent to (almost all of) the rest of the application. 35 | */ 36 | basePath: pathPrefix, 37 | 38 | images: { 39 | loader: 'custom', 40 | }, 41 | 42 | /** 43 | * Set custom `process.env.SOMETHING` values to use in the application. 44 | * You can do this with Webpack's `DefinePlugin`, but this is more concise. 45 | * It's also possible to provide values via `publicRuntimeConfig`, but 46 | * this method is preferred as it can be done statically at build time. 47 | * 48 | * @see https://nextjs.org/docs/api-reference/next.config.js/environment-variables 49 | */ 50 | env: { 51 | PATH_PREFIX: pathPrefix, 52 | THEME_CONFIG: JSON.stringify(themeConfig), 53 | }, 54 | 55 | /** 56 | * Next.js reports TypeScript errors by default. If you don't want to 57 | * leverage this behavior and prefer something else instead, like your 58 | * editor's integration, you may want to disable it. 59 | */ 60 | // typescript: { 61 | // ignoreDevErrors: true, 62 | // }, 63 | 64 | /** Customises the build */ 65 | webpack(config, { isServer }) { 66 | // EUI uses some libraries and features that don't work outside of a 67 | // browser by default. We need to configure the build so that these 68 | // features are either ignored or replaced with stub implementations. 69 | if (isServer) { 70 | config.externals = config.externals.map(eachExternal => { 71 | if (typeof eachExternal !== 'function') { 72 | return eachExternal; 73 | } 74 | 75 | return (context, callback) => { 76 | if (context.request.indexOf('@elastic/eui') > -1) { 77 | return callback(); 78 | } 79 | 80 | return eachExternal(context, callback); 81 | }; 82 | }); 83 | 84 | // Mock HTMLElement on the server-side 85 | const definePluginId = config.plugins.findIndex( 86 | p => p.constructor.name === 'DefinePlugin' 87 | ); 88 | 89 | config.plugins[definePluginId].definitions = { 90 | ...config.plugins[definePluginId].definitions, 91 | HTMLElement: function () {}, 92 | }; 93 | } 94 | 95 | // Copy theme CSS files into `public` 96 | config.plugins.push( 97 | new CopyWebpackPlugin({ patterns: themeConfig.copyConfig }), 98 | 99 | // Moment ships with a large number of locales. Exclude them, leaving 100 | // just the default English locale. If you need other locales, see: 101 | // https://create-react-app.dev/docs/troubleshooting/#momentjs-locales-are-missing 102 | new IgnorePlugin({ 103 | resourceRegExp: /^\.\/locale$/, 104 | contextRegExp: /moment$/, 105 | }) 106 | ); 107 | 108 | config.resolve.mainFields = ['module', 'main']; 109 | 110 | return config; 111 | }, 112 | }; 113 | 114 | /** 115 | * Enhances the Next config with the ability to: 116 | * - Analyze the webpack bundle 117 | * - Load images from JavaScript. 118 | * - Load SCSS files from JavaScript. 119 | */ 120 | module.exports = withBundleAnalyzer({ 121 | enabled: process.env.ANALYZE === 'true', 122 | })(nextConfig); 123 | 124 | /** 125 | * Find all EUI themes and construct a theme configuration object. 126 | * 127 | * The `copyConfig` key is used to configure CopyWebpackPlugin, which 128 | * copies the default EUI themes into the `public` directory, injecting a 129 | * hash into the filename so that when EUI is updated, new copies of the 130 | * themes will be fetched. 131 | * 132 | * The `availableThemes` key is used in the app to includes the themes in 133 | * the app's `` element, and for theme switching. 134 | * 135 | * @return {ThemeConfig} 136 | */ 137 | function buildThemeConfig() { 138 | const themeFiles = glob.sync( 139 | path.join( 140 | __dirname, 141 | 'node_modules', 142 | '@elastic', 143 | 'eui', 144 | 'dist', 145 | 'eui_theme_*.min.css' 146 | ) 147 | ); 148 | 149 | const themeConfig = { 150 | availableThemes: [], 151 | copyConfig: [], 152 | }; 153 | 154 | for (const each of themeFiles) { 155 | const basename = path.basename(each, '.min.css'); 156 | 157 | const themeId = basename.replace(/^eui_theme_/, ''); 158 | 159 | const themeName = 160 | themeId[0].toUpperCase() + themeId.slice(1).replace(/_/g, ' '); 161 | 162 | const publicPath = `themes/${basename}.${hashFile(each)}.min.css`; 163 | const toPath = path.join( 164 | __dirname, 165 | `public`, 166 | `themes`, 167 | `${basename}.${hashFile(each)}.min.css` 168 | ); 169 | 170 | themeConfig.availableThemes.push({ 171 | id: themeId, 172 | name: themeName, 173 | publicPath, 174 | }); 175 | 176 | themeConfig.copyConfig.push({ 177 | from: each, 178 | to: toPath, 179 | }); 180 | } 181 | 182 | return themeConfig; 183 | } 184 | 185 | /** 186 | * Given a file, calculate a hash and return the first portion. The number 187 | * of characters is truncated to match how Webpack generates hashes. 188 | * 189 | * @param {string} filePath the absolute path to the file to hash. 190 | * @return string 191 | */ 192 | function hashFile(filePath) { 193 | const hash = crypto.createHash(`sha256`); 194 | const fileData = fs.readFileSync(filePath); 195 | hash.update(fileData); 196 | const fullHash = hash.digest(`hex`); 197 | 198 | // Use a hash length that matches what Webpack does 199 | return fullHash.substr(0, 20); 200 | } 201 | 202 | /** 203 | * This starter assumes that if `usePathPrefix` is true, then you're serving the site 204 | * on GitHub pages. If that isn't the case, then you can simply replace the call to 205 | * this function with whatever is the correct path prefix. 206 | * 207 | * The implementation attempts to derive a path prefix for serving up a static site by 208 | * looking at the following in order. 209 | * 210 | * 1. The git config for "origin" 211 | * 2. The `name` field in `package.json` 212 | * 213 | * Really, the first should be sufficient and correct for a GitHub Pages site, because the 214 | * repository name is what will be used to serve the site. 215 | */ 216 | function derivePathPrefix() { 217 | const gitConfigPath = path.join(__dirname, '.git', 'config'); 218 | 219 | if (fs.existsSync(gitConfigPath)) { 220 | const gitConfig = iniparser.parseSync(gitConfigPath); 221 | 222 | if (gitConfig['remote "origin"'] != null) { 223 | const originUrl = gitConfig['remote "origin"'].url; 224 | 225 | // eslint-disable-next-line prettier/prettier 226 | return '/' + originUrl.split('/').pop().replace(/\.git$/, ''); 227 | } 228 | } 229 | 230 | const packageJsonPath = path.join(__dirname, 'package.json'); 231 | 232 | if (fs.existsSync(packageJsonPath)) { 233 | const { name: packageName } = require(packageJsonPath); 234 | // Strip out any username / namespace part. This works even if there is 235 | // no username in the package name. 236 | return '/' + packageName.split('/').pop(); 237 | } 238 | 239 | throw new Error( 240 | "Can't derive path prefix, as neither .git/config nor package.json exists" 241 | ); 242 | } 243 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "detection-rules-explorer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "prebuild": "tsc parseRuleData.ts && node parseRuleData.js", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "export": "next export" 12 | }, 13 | "dependencies": { 14 | "@elastic/eui": "^68.0.0", 15 | "@emotion/cache": "^11.10.3", 16 | "@emotion/react": "^11.10.4", 17 | "core-js": "^3.25.1", 18 | "react-lazy-load": "^4.0.1", 19 | "regenerator-runtime": "^0.13.9", 20 | "tar": "^6.1.15", 21 | "toml": "^3.0.0" 22 | }, 23 | "devDependencies": { 24 | "@elastic/datemath": "^5.0.3", 25 | "@emotion/babel-plugin": "^11.10.2", 26 | "@next/bundle-analyzer": "^12.3.4", 27 | "@types/node": "^16.11.10", 28 | "@typescript-eslint/eslint-plugin": "^5.5.0", 29 | "axios": "^1.4.0", 30 | "copy-webpack-plugin": "^10.0.0", 31 | "eslint": "<8.0.0", 32 | "eslint-config-next": "12.0.4", 33 | "eslint-config-prettier": "^8.3.0", 34 | "eslint-plugin-prettier": "^4.0.0", 35 | "glob": "^7.2.0", 36 | "iniparser": "^1.0.5", 37 | "moment": "^2.29.4", 38 | "next": "^12.3.1", 39 | "null-loader": "^4.0.1", 40 | "prettier": "^2.5.0", 41 | "react": "^17.0.2", 42 | "react-dom": "^17.0.2", 43 | "sass": "^1.43.5", 44 | "serve": "^13.0.2", 45 | "typescript": "^4.5.2", 46 | "typescript-plugin-css-modules": "^3.4.0" 47 | }, 48 | "resolutions": { 49 | "trim": "0.0.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /parseRuleData.ts: -------------------------------------------------------------------------------- 1 | import * as tar from 'tar'; 2 | import { PassThrough } from 'stream'; 3 | 4 | import * as toml from 'toml'; 5 | import * as fs from 'fs'; 6 | import axios from 'axios'; 7 | 8 | interface RuleSummary { 9 | id: string; 10 | name: string; 11 | tags: Array; 12 | updated_date: Date; 13 | } 14 | 15 | interface TagSummary { 16 | tag_type: string; 17 | tag_name: string; 18 | tag_full: string; 19 | count: number; 20 | } 21 | 22 | function addTagSummary(t: string, tagSummaries: Map) { 23 | const parts = t.split(': '); 24 | let s = tagSummaries.get(t); 25 | if (s == undefined) { 26 | s = { 27 | tag_type: parts[0], 28 | tag_name: parts[1], 29 | tag_full: t, 30 | count: 0, 31 | }; 32 | } 33 | s.count++; 34 | tagSummaries.set(t, s); 35 | } 36 | 37 | const RULES_OUTPUT_PATH = './src/data/rules/'; 38 | 39 | async function getPrebuiltDetectionRules( 40 | ruleSummaries: RuleSummary[], 41 | tagSummaries: Map 42 | ) { 43 | let count = 0; 44 | type Technique = { 45 | id: string; 46 | name: string; 47 | reference: string; 48 | subtechnique?: { id: string; reference: string }[]; 49 | }; 50 | 51 | type Tactic = { 52 | id: string; 53 | name: string; 54 | reference: string; 55 | }; 56 | 57 | type Threat = { 58 | framework: string; 59 | technique?: Technique[]; 60 | tactic?: Tactic; 61 | }; 62 | 63 | const convertHuntMitre = function (mitreData: string[]): Threat[] { 64 | const threat: Threat[] = []; 65 | 66 | mitreData.forEach((item) => { 67 | if (item.startsWith('TA')) { 68 | threat.push({ 69 | framework: "MITRE ATT&CK", 70 | tactic: { 71 | id: item, 72 | name: "", 73 | reference: `https://attack.mitre.org/tactics/${item}/`, 74 | }, 75 | technique: [], // Ensure technique is an empty array if not present 76 | }); 77 | } else if (item.startsWith('T')) { 78 | const parts = item.split('.'); 79 | const techniqueId = parts[0]; 80 | const subtechniqueId = parts[1]; 81 | 82 | const technique: Technique = { 83 | id: techniqueId, 84 | name: "", 85 | reference: `https://attack.mitre.org/techniques/${techniqueId}/`, 86 | }; 87 | 88 | if (subtechniqueId) { 89 | technique.subtechnique = [ 90 | { 91 | id: `${techniqueId}.${subtechniqueId}`, 92 | reference: `https://attack.mitre.org/techniques/${techniqueId}/${subtechniqueId}/`, 93 | }, 94 | ]; 95 | } 96 | 97 | // Find the last added threat with a tactic to add the technique to it 98 | const lastThreat = threat[threat.length - 1]; 99 | if (lastThreat && lastThreat.tactic && lastThreat.technique) { 100 | lastThreat.technique.push(technique); 101 | } else { 102 | threat.push({ 103 | framework: "MITRE ATT&CK", 104 | tactic: { 105 | id: "", 106 | name: "", 107 | reference: "", 108 | }, 109 | technique: [technique], 110 | }); 111 | } 112 | } 113 | }); 114 | 115 | return threat; 116 | }; 117 | 118 | const addRule = function (buffer) { 119 | const ruleContent = toml.parse(buffer); 120 | 121 | // Check if ruleContent.rule and ruleContent.hunt exist 122 | const ruleId = ruleContent.rule?.rule_id || ruleContent.hunt?.uuid; 123 | if (!ruleId) { 124 | throw new Error('Neither rule_id nor hunt.uuid is available'); 125 | } 126 | 127 | // Initialize ruleContent.rule and ruleContent.metadata if they are undefined 128 | ruleContent.rule = ruleContent.rule || {}; 129 | ruleContent.metadata = ruleContent.metadata || {}; 130 | 131 | // Helper function to set default values if they do not exist 132 | const setDefault = (obj, key, defaultValue) => { 133 | if (!obj[key]) { 134 | obj[key] = defaultValue; 135 | } 136 | }; 137 | 138 | // Use default tags if ruleContent.rule.tags does not exist 139 | const tags = ruleContent.rule.tags || ["Hunt Type: Hunt"]; 140 | setDefault(ruleContent.rule, 'tags', ["Hunt Type: Hunt"]); 141 | 142 | // Add a tag based on the language 143 | const language = ruleContent.rule?.language; 144 | if (language) { 145 | tags.push(`Language: ${language}`); 146 | } 147 | 148 | // Add creation_date and updated_date if they do not exist 149 | const defaultDate = new Date(0).toISOString(); 150 | setDefault(ruleContent.metadata, 'creation_date', defaultDate); 151 | setDefault(ruleContent.metadata, 'updated_date', defaultDate); 152 | 153 | // Use current date as default updated_date if it does not exist 154 | const updatedDate = new Date(ruleContent.metadata.updated_date.replace(/\//g, '-')); 155 | 156 | // Use hunt.name if rule.name does not exist 157 | const ruleName = ruleContent.rule.name || ruleContent.hunt.name || 'Unknown Rule'; 158 | 159 | // Set other default values if they do not exist 160 | setDefault(ruleContent.metadata, 'integration', ruleContent.hunt?.integration); 161 | setDefault(ruleContent.rule, 'query', ruleContent.hunt?.query); 162 | setDefault(ruleContent.rule, 'license', "Elastic License v2"); 163 | setDefault(ruleContent.rule, 'description', ruleContent.hunt?.description); 164 | 165 | // Convert hunt.mitre to rule.threat if hunt.mitre exists 166 | if (ruleContent.hunt?.mitre) { 167 | ruleContent.rule.threat = convertHuntMitre(ruleContent.hunt.mitre); 168 | } 169 | 170 | ruleSummaries.push({ 171 | id: ruleId, 172 | name: ruleName, 173 | tags: tags, 174 | updated_date: updatedDate, 175 | }); 176 | 177 | for (const t of tags) { 178 | addTagSummary(t, tagSummaries); 179 | } 180 | 181 | fs.writeFileSync( 182 | `${RULES_OUTPUT_PATH}${ruleId}.json`, 183 | JSON.stringify(ruleContent) 184 | ); 185 | 186 | count++; 187 | }; 188 | 189 | const githubRulesTarballUrl = 190 | 'https://api.github.com/repos/elastic/detection-rules/tarball'; 191 | const res = await axios.get(githubRulesTarballUrl, { 192 | responseType: 'stream', 193 | }); 194 | const parser = res.data.pipe(new tar.Parse()); 195 | parser.on('entry', entry => { 196 | if ( 197 | (entry.path.match(/^elastic-detection-rules-.*\/rules\/.*\.toml$/) || 198 | entry.path.match(/^elastic-detection-rules-.*\/hunting\/.*\.toml$/) || 199 | entry.path.match( 200 | /^elastic-detection-rules-.*\/rules_building_block\/.*\.toml$/ 201 | )) && 202 | !entry.path.match(/\/_deprecated\//) 203 | ) { 204 | const contentStream = new PassThrough(); 205 | entry.pipe(contentStream); 206 | let buf = Buffer.alloc(0); 207 | contentStream.on('data', function (d) { 208 | buf = Buffer.concat([buf, d]); 209 | }); 210 | contentStream.on('end', () => { 211 | addRule(buf); 212 | }); 213 | } else { 214 | entry.resume(); 215 | } 216 | }); 217 | await new Promise(resolve => parser.on('finish', resolve)); 218 | 219 | console.log(`loaded ${count} rules from prebuilt repository`); 220 | } 221 | 222 | const integrationsTagMap = new Map([ 223 | ['Living off the Land', 'Tactic: Defensive Evasion'], 224 | ['DGA', 'Tactic: Command and Control'], 225 | ['Lateral Movement Detection', 'Tactic: Lateral Movement'], 226 | ['Data Exfiltration', 'Tactic: Exfiltration'], 227 | ['Host', 'Domain: Endpoint'], 228 | ['User', 'Domain: User'], 229 | ['ML', 'Rule Type: Machine Learning'], 230 | ]); 231 | 232 | async function getPackageRules( 233 | name: string, 234 | displayName: string, 235 | ruleSummaries: RuleSummary[], 236 | tagSummaries: Map 237 | ) { 238 | const githubRulesListUrl = `https://api.github.com/repos/elastic/integrations/contents/packages/${name}/kibana/security_rule`; 239 | const githubRulesCommitsUrl = `https://api.github.com/repos/elastic/integrations/commits?path=packages%2F${name}%2Fkibana%2Fsecurity_rule&page=1&per_page=1`; 240 | 241 | const rulesCommitsResponse = await axios.get(githubRulesCommitsUrl); 242 | const updatedDate = new Date( 243 | rulesCommitsResponse.data[0].commit.committer.date 244 | ); 245 | const ruleListResponse = await axios.get(githubRulesListUrl); 246 | 247 | for (const r of ruleListResponse.data) { 248 | const ruleContent = await axios.get(r.download_url); 249 | 250 | const tags = ruleContent.data.attributes.tags 251 | .filter(x => x != 'Elastic') 252 | .map(x => { 253 | if (integrationsTagMap.has(x)) { 254 | return integrationsTagMap.get(x); 255 | } else { 256 | return x; 257 | } 258 | }); 259 | tags.push('Use Case: Threat Detection'); 260 | 261 | // for now, map the tags to look more like the prebuild rules package 262 | ruleSummaries.push({ 263 | id: ruleContent.data.id, 264 | name: ruleContent.data.attributes.name, 265 | tags: tags, 266 | updated_date: updatedDate, 267 | }); 268 | for (const t of tags) { 269 | addTagSummary(t, tagSummaries); 270 | } 271 | const mappedRuleContent = { 272 | metadata: { 273 | updated_date: updatedDate, 274 | source_integration: name, 275 | source_integration_name: displayName, 276 | }, 277 | rule: ruleContent.data.attributes, 278 | }; 279 | mappedRuleContent.rule.tags = tags; 280 | fs.writeFileSync( 281 | `${RULES_OUTPUT_PATH}${ruleContent.data.id}.json`, 282 | JSON.stringify(mappedRuleContent) 283 | ); 284 | } 285 | console.log( 286 | `loaded ${ruleListResponse.data.length} rules from integration package '${name}'` 287 | ); 288 | } 289 | 290 | async function precomputeRuleSummaries() { 291 | const ruleSummaries: RuleSummary[] = []; 292 | 293 | const tagSummaries = new Map(); 294 | 295 | fs.mkdirSync(RULES_OUTPUT_PATH, { recursive: true }); 296 | 297 | await getPrebuiltDetectionRules(ruleSummaries, tagSummaries); 298 | 299 | console.log(`loaded ${ruleSummaries.length} rules`); 300 | console.log(`example rule:`); 301 | console.log(ruleSummaries[0]); 302 | console.log(`found ${tagSummaries.size} tags`); 303 | console.log(`example tag:`); 304 | console.log(tagSummaries.get('Data Source: APM')); 305 | 306 | const newestRules = ruleSummaries.sort( 307 | (a, b) => b.updated_date.getTime() - a.updated_date.getTime() 308 | ); 309 | console.log( 310 | `Parsed ${newestRules.length} rules. Newest rule is '${newestRules[0].name}', updated '${newestRules[0].updated_date}'.` 311 | ); 312 | 313 | fs.writeFileSync('./src/data/newestRules.json', JSON.stringify(newestRules)); 314 | 315 | const popularTags = Array.from(tagSummaries.values()).sort( 316 | (a, b) => b.count - a.count 317 | ); 318 | console.log( 319 | `Parsed ${popularTags.length} tags. Most popular tag is '${popularTags[0].tag_full}' with '${popularTags[0].count}' rules.` 320 | ); 321 | 322 | fs.writeFileSync('./src/data/tagSummaries.json', JSON.stringify(popularTags)); 323 | } 324 | 325 | (async () => { 326 | await precomputeRuleSummaries(); 327 | })(); 328 | -------------------------------------------------------------------------------- /public/images/404_rainy_cloud_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/404_rainy_cloud_dark.png -------------------------------------------------------------------------------- /public/images/404_rainy_cloud_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/404_rainy_cloud_light.png -------------------------------------------------------------------------------- /public/images/favicon/dev/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/favicon/dev/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon/dev/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/favicon/dev/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/favicon/dev/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/favicon/dev/favicon-96x96.png -------------------------------------------------------------------------------- /public/images/favicon/prod/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/favicon/prod/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon/prod/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/favicon/prod/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/favicon/prod/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/public/images/favicon/prod/favicon-96x96.png -------------------------------------------------------------------------------- /public/images/home/illustration-eui-hero-500-shadow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/images/logo-eui.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/patterns/pattern-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /src/components/chrome/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { EuiProvider, EuiThemeColorMode } from '@elastic/eui'; 4 | 5 | import { useTheme } from '../theme'; 6 | 7 | import createCache from '@emotion/cache'; 8 | 9 | /** 10 | * Renders the UI that surrounds the page content. 11 | */ 12 | const Chrome: FunctionComponent = ({ children }) => { 13 | const { colorMode } = useTheme(); 14 | 15 | /** 16 | * This `@emotion/cache` instance is used to insert the global styles 17 | * into the correct location in ``. Otherwise they would be 18 | * inserted after the static CSS files, resulting in style clashes. 19 | * Only necessary until EUI has converted all components to CSS-in-JS: 20 | * https://github.com/elastic/eui/issues/3912 21 | */ 22 | const defaultCache = createCache({ 23 | key: 'eui', 24 | container: 25 | typeof document !== 'undefined' 26 | ? document.querySelector('meta[name="eui-styles"]') 27 | : null, 28 | }); 29 | const utilityCache = createCache({ 30 | key: 'util', 31 | container: 32 | typeof document !== 'undefined' 33 | ? document.querySelector('meta[name="eui-styles-utility"]') 34 | : null, 35 | }); 36 | 37 | return ( 38 | 41 | {children} 42 | 43 | ); 44 | }; 45 | 46 | export default Chrome; 47 | -------------------------------------------------------------------------------- /src/components/chrome/theme_switcher.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { EuiButtonIcon } from '@elastic/eui'; 3 | import { useTheme } from '../theme'; 4 | 5 | /** 6 | * Current theme is set in localStorage 7 | * so that it persists between visits. 8 | */ 9 | const ThemeSwitcher: FunctionComponent = () => { 10 | const { colorMode, setColorMode } = useTheme(); 11 | const isDarkTheme = colorMode === 'dark'; 12 | 13 | const handleChangeTheme = (newTheme: string) => { 14 | setColorMode(newTheme); 15 | }; 16 | 17 | return ( 18 | 23 | handleChangeTheme(isDarkTheme ? 'light' : 'dark') 24 | }> 25 | ); 26 | }; 27 | 28 | export default ThemeSwitcher; 29 | -------------------------------------------------------------------------------- /src/components/details/rule_details.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const ruleDetailsStyles = euiTheme => ({ 4 | container: css` 5 | max-width: 1200px; 6 | width: 100%; 7 | margin: auto !important; 8 | } 9 | `, 10 | badge: css` 11 | margin: 4px; 12 | `, 13 | list: css` 14 | > dt { 15 | width: 40%; 16 | } 17 | > dd { 18 | width: 60%; 19 | } 20 | `, 21 | callout: css` 22 | max-width: 1200px; 23 | margin: auto; 24 | padding-top: ${euiTheme.size.base}; 25 | text-align: center; 26 | `, 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/home/gradient_bg.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const gradientBgStyles = backgroundColors => ({ 4 | gradientBg: css` 5 | position: relative; 6 | padding-top: 48px; // top nav 7 | min-height: 100vh; 8 | background: radial-gradient( 9 | circle 600px at top left, 10 | ${backgroundColors.topLeft}, 11 | transparent 12 | ), 13 | radial-gradient( 14 | circle 800px at 600px 200px, 15 | ${backgroundColors.centerTop}, 16 | transparent 17 | ), 18 | radial-gradient( 19 | circle 800px at top right, 20 | ${backgroundColors.topRight}, 21 | transparent 22 | ), 23 | radial-gradient( 24 | circle 800px at left center, 25 | ${backgroundColors.centerMiddleLeft}, 26 | transparent 27 | ), 28 | radial-gradient( 29 | circle 800px at right center, 30 | ${backgroundColors.centerMiddleRight}, 31 | transparent 32 | ), 33 | radial-gradient( 34 | circle 800px at right bottom, 35 | ${backgroundColors.bottomRight}, 36 | transparent 37 | ), 38 | radial-gradient( 39 | circle 800px at left bottom, 40 | ${backgroundColors.bottomLeft}, 41 | transparent 42 | ); 43 | `, 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/home/gradient_bg.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { useEuiTheme, transparentize } from '@elastic/eui'; 3 | import { useTheme } from '../theme'; 4 | import { gradientBgStyles } from './gradient_bg.styles'; 5 | 6 | const GradientBg: FunctionComponent = ({ children }) => { 7 | const { euiTheme } = useEuiTheme(); 8 | const { colorMode } = useTheme(); 9 | 10 | const alpha = colorMode === 'dark' ? 0.03 : 0.05; 11 | 12 | const backgroundColors = { 13 | topLeft: transparentize(euiTheme.colors.success, alpha), 14 | centerTop: transparentize(euiTheme.colors.accent, alpha), 15 | topRight: transparentize(euiTheme.colors.warning, alpha), 16 | centerMiddleLeft: transparentize(euiTheme.colors.warning, alpha), 17 | centerMiddleRight: transparentize(euiTheme.colors.accent, alpha), 18 | bottomRight: transparentize(euiTheme.colors.primary, alpha), 19 | bottomLeft: transparentize(euiTheme.colors.accent, alpha), 20 | }; 21 | 22 | const styles = gradientBgStyles(backgroundColors); 23 | 24 | return
{children}
; 25 | }; 26 | 27 | export default GradientBg; 28 | -------------------------------------------------------------------------------- /src/components/home/header.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const headerStyles = euiTheme => ({ 4 | logo: css` 5 | display: inline-flex; 6 | flex-wrap: wrap; 7 | gap: ${euiTheme.size.m}; 8 | `, 9 | title: css` 10 | line-height: 1.75; // Measured in the browser 11 | `, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/home/header.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import { 4 | EuiHeader, 5 | EuiTitle, 6 | EuiHeaderSectionItemButton, 7 | useEuiTheme, 8 | EuiToolTip, 9 | EuiIcon, 10 | } from '@elastic/eui'; 11 | import { imageLoader } from '../../lib/loader'; 12 | import ThemeSwitcher from './theme_switcher'; 13 | import { headerStyles } from './header.styles'; 14 | import Logo from '../../../public/images/logo-eui.svg'; 15 | 16 | const Header = () => { 17 | const { euiTheme } = useEuiTheme(); 18 | const href = 'https://github.com/elastic/detection-rules'; 19 | const label = 'EUI GitHub repo'; 20 | const styles = headerStyles(euiTheme); 21 | 22 | return ( 23 | 29 | 30 | 31 | 32 | Elastic Security Detection Rules 33 | 34 | 35 | , 36 | ], 37 | borders: 'none', 38 | }, 39 | { 40 | items: [ 41 | , 42 | 43 | 44 | 46 | , 47 | ], 48 | borders: 'none', 49 | }, 50 | ]} 51 | /> 52 | ); 53 | }; 54 | 55 | export default Header; 56 | -------------------------------------------------------------------------------- /src/components/home/home_hero.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const homeHeroStyles = euiTheme => ({ 4 | container: css` 5 | max-width: 1000px; 6 | margin: auto !important; 7 | 8 | @media (max-width: ${euiTheme.breakpoint.m}px) { 9 | text-align: center; 10 | 11 | > .euiFlexItem:first-of-type { 12 | order: 2; 13 | } 14 | } 15 | 16 | text-align: center; 17 | `, 18 | title: css` 19 | @media (min-width: ${euiTheme.breakpoint.m}px) { 20 | padding-top: ${euiTheme.size.base}; 21 | } 22 | `, 23 | subtitle: css` 24 | margin-top: ${euiTheme.size.l}; 25 | padding-bottom: ${euiTheme.size.m}; 26 | `, 27 | description: css` 28 | @media (max-width: ${euiTheme.breakpoint.m}px) { 29 | align-self: center; 30 | } 31 | `, 32 | aligned: css` 33 | vertical-align: middle; 34 | margin-right: 3px; 35 | font-weight: bold; 36 | `, 37 | accordian: css` 38 | margin: auto; 39 | 40 | .euiAccordion__triggerWrapper { 41 | display: inline-flex; 42 | } 43 | 44 | button { 45 | flex-grow: 0; 46 | inline-size: auto; 47 | } 48 | `, 49 | search: css` 50 | width: 500px; 51 | margin: auto; 52 | `, 53 | grid: css` 54 | justify-content: center; 55 | `, 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/home/home_hero.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useState, useRef } from 'react'; 2 | import { 3 | EuiFlexGroup, 4 | EuiFlexItem, 5 | EuiTitle, 6 | EuiText, 7 | EuiSpacer, 8 | EuiLink, 9 | EuiPanel, 10 | EuiFlexGrid, 11 | EuiFieldSearch, 12 | EuiAccordion, 13 | EuiFormRow, 14 | useGeneratedHtmlId, 15 | } from '@elastic/eui'; 16 | import { homeHeroStyles } from './home_hero.styles'; 17 | import { useEuiTheme } from '@elastic/eui'; 18 | 19 | import RuleFilter from './rule_filter'; 20 | 21 | import { RuleSummary, TagSummary } from '../../types'; 22 | 23 | interface RuleFilterProps { 24 | rules: RuleSummary[]; 25 | tagSummaries: TagSummary[]; 26 | searchFilter: string; 27 | tagFilter: string[]; 28 | onSearchChange: (e: string) => void; 29 | onTagChange: (type: string, selected: string[]) => void; 30 | } 31 | 32 | const HomeHero: FunctionComponent = ({ 33 | children, 34 | rules, 35 | tagSummaries, 36 | searchFilter, 37 | tagFilter, 38 | onSearchChange, 39 | onTagChange, 40 | }) => { 41 | const { euiTheme } = useEuiTheme(); 42 | const styles = homeHeroStyles(euiTheme); 43 | 44 | const [displaySearchTerm, setDisplaySearchTerm] = useState(''); 45 | const searchUpdateTimeout = useRef(null); 46 | 47 | const onSearchBoxChange = function (e) { 48 | setDisplaySearchTerm(e.target.value); 49 | if (searchUpdateTimeout.current) { 50 | clearTimeout(searchUpdateTimeout.current); 51 | } 52 | searchUpdateTimeout.current = setTimeout(() => { 53 | onSearchChange(e.target.value); 54 | }, 100); 55 | }; 56 | 57 | return ( 58 | 59 | 60 | 61 |

Elastic Security Detection Rules

62 |
63 | 64 | 65 | 66 | 67 |

68 | Elastic Security detection rules help users to set up and get their 69 | detections and security monitoring going as soon as possible. 70 | Elastic is committed to{' '} 71 | 74 | transparency and openness 75 | {' '} 76 | with the security community, which is why we build and maintain our 77 | detection logic publicly. 78 |

79 |

80 | See our{' '} 81 | 84 | docs 85 | {' '} 86 | for more information on how to enable these detection rules in 87 | Elastic Security. 88 |

89 |
90 | 91 | 92 | 93 | 94 | onSearchBoxChange(e)} 98 | fullWidth 99 | /> 100 | 101 | 102 | 103 | 104 | 105 | x.tag_type == 'Domain')} 109 | tagFilter={tagFilter} 110 | onTagChange={onTagChange} 111 | /> 112 | 113 | x.tag_type == 'Rule Type' && x.tag_name != 'ML' 118 | )} 119 | tagFilter={tagFilter} 120 | onTagChange={onTagChange} 121 | /> 122 | 123 | x.tag_type == 'OS')} 127 | tagFilter={tagFilter} 128 | onTagChange={onTagChange} 129 | /> 130 | 131 | x.tag_type == 'Use Case')} 135 | tagFilter={tagFilter} 136 | onTagChange={onTagChange} 137 | /> 138 | 139 | x.tag_type == 'Tactic')} 143 | tagFilter={tagFilter} 144 | onTagChange={onTagChange} 145 | /> 146 | 147 | x.tag_type == 'Data Source')} 151 | tagFilter={tagFilter} 152 | onTagChange={onTagChange} 153 | /> 154 | 155 | x.tag_type == 'Hunt Type')} 159 | tagFilter={tagFilter} 160 | onTagChange={onTagChange} 161 | /> 162 | x.tag_type == 'Language')} 166 | tagFilter={tagFilter} 167 | onTagChange={onTagChange} 168 | /> 169 | 170 |
171 |
172 | ); 173 | }; 174 | 175 | export default HomeHero; 176 | -------------------------------------------------------------------------------- /src/components/home/paste.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 | {rule.name} 7 |

8 |
9 | 10 | {rule.tags.map((t, j) => { 11 | let color = 'hollow'; 12 | let icon = ''; 13 | if (t.startsWith('Domain')) { 14 | color = 'accent'; 15 | icon = 'globe'; 16 | } 17 | if (t.startsWith('Use Case')) { 18 | color = 'primary'; 19 | icon = 'launch'; 20 | } 21 | if (t.startsWith('Data Source')) { 22 | color = 'default'; 23 | icon = 'database'; 24 | } 25 | if (t.startsWith('OS')) { 26 | color = 'success'; 27 | icon = 'compute'; 28 | } 29 | if (t.startsWith('Tactic')) { 30 | color = 'warning'; 31 | icon = 'bug'; 32 | } 33 | return ( 34 | 39 | {t} 40 | 41 | ); 42 | })} 43 | 44 | 45 |

46 | Updated {moment(rule.updated_date).fromNow()} 47 |

48 |
49 |
50 |
51 | ); -------------------------------------------------------------------------------- /src/components/home/rule_filter.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const ruleFilterStyles = () => ({ 4 | aligned: css` 5 | vertical-align: middle; 6 | margin-right: 3px; 7 | `, 8 | combo: css` 9 | padding-top: 5px; 10 | width: 100%; 11 | `, 12 | panel: css` 13 | margin: 10px; 14 | padding: 10px; 15 | 16 | width: 300px; 17 | flex-grow: 0; 18 | 19 | text-align: center; 20 | justify-content: center; 21 | align-content: start; 22 | `, 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/home/rule_filter.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useState } from 'react'; 2 | import { 3 | EuiPanel, 4 | EuiHealth, 5 | EuiText, 6 | EuiIcon, 7 | EuiComboBox, 8 | } from '@elastic/eui'; 9 | import { ruleFilterStyles } from './rule_filter.styles'; 10 | import { TagSummary } from '../../types'; 11 | import { ruleFilterTypeMap } from '../../lib/ruledata'; 12 | 13 | interface RuleFilterProps { 14 | tagList: TagSummary[]; 15 | tagFilter: string[]; 16 | displayName: string; 17 | icon: string; 18 | onTagChange: (type: string, selected: string[]) => void; 19 | } 20 | 21 | const RuleFilter: FunctionComponent = ({ 22 | children, 23 | tagList, 24 | tagFilter, 25 | displayName, 26 | icon, 27 | onTagChange, 28 | }) => { 29 | const styles = ruleFilterStyles(); 30 | 31 | const options = tagList.map(t => { 32 | return { 33 | value: t, 34 | label: `${t.tag_name} (${t.count})`, 35 | color: ruleFilterTypeMap[t.tag_type].color, 36 | }; 37 | }); 38 | 39 | const selectedOptions = options.filter(o => { 40 | return tagFilter.includes(o.value.tag_full); 41 | }); 42 | 43 | const typeName = tagList.length > 0 ? tagList[0].tag_type : ''; 44 | 45 | return ( 46 | 47 | 48 |

49 | 50 | {displayName} 51 |

52 |
53 | o.value.count > 0).length 57 | } ${displayName}`} 58 | options={options} 59 | selectedOptions={selectedOptions} 60 | isClearable={true} 61 | onChange={selected => { 62 | onTagChange( 63 | typeName, 64 | selected.map(o => o.value.tag_full) 65 | ); 66 | }} 67 | renderOption={o => { 68 | return ( 69 | 0 ? o.color : '#eeeeee'}> 70 | {o.label} 71 | 72 | ); 73 | }} 74 | /> 75 |
76 | ); 77 | /* 78 | return ( 79 | <> 80 | 81 |

82 | 83 | {displayName} 84 |

85 |
86 | 87 | {tagList.map((t, i) => { 88 | const isTagged = tagFilter.filter(x => x == t.tag_full).length > 0; 89 | let badgeTheme = ruleFilterTypeMap[t.tag_type] || { 90 | color: 'hollow', 91 | }; 92 | if (!isTagged) { 93 | badgeTheme = { color: 'hollow' }; 94 | } 95 | if (t.count == 0) { 96 | badgeTheme = { color: 'default' }; 97 | } 98 | return ( 99 | 100 | { 103 | console.log(`${t.tag_full} ${isTagged}`); 104 | if (isTagged) { 105 | onTagChange([], [t.tag_full]); 106 | } else { 107 | onTagChange([t.tag_full], []); 108 | } 109 | }} 110 | onClickAriaLabel={`Toggle tag for ${t.tag_full}`}> 111 | {t.tag_name} ({t.count}) 112 | 113 | 114 | ); 115 | })} 116 | 117 | 118 | );*/ 119 | }; 120 | 121 | export default RuleFilter; 122 | -------------------------------------------------------------------------------- /src/components/home/rule_list.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const ruleListStyles = euiTheme => ({ 4 | grid: css` 5 | padding-top: ${euiTheme.size.base}; 6 | padding-bottom: ${euiTheme.size.base}; 7 | 8 | text-align: center; 9 | justify-content: center; 10 | align-content: start; 11 | 12 | min-height: 1000px; 13 | `, 14 | callout: css` 15 | max-width: 800px; 16 | margin: auto; 17 | padding-top: ${euiTheme.size.base}; 18 | text-align: center; 19 | `, 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/home/rule_list.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from 'react'; 2 | import { EuiFlexGrid, EuiCallOut } from '@elastic/eui'; 3 | import { ruleListStyles } from './rule_list.styles'; 4 | import { useEuiTheme } from '@elastic/eui'; 5 | 6 | import RulePanel from './rule_panel'; 7 | 8 | import { RuleSummary } from '../../types'; 9 | 10 | interface RuleListProps { 11 | rules: RuleSummary[]; 12 | } 13 | 14 | const MAX_RULES = 100; 15 | 16 | const RuleList: FunctionComponent = ({ children, rules }) => { 17 | const { euiTheme } = useEuiTheme(); 18 | const styles = ruleListStyles(euiTheme); 19 | 20 | const ruleSlice = useMemo(() => { 21 | return rules.slice(0, MAX_RULES); 22 | }, [rules]); 23 | 24 | return ( 25 | <> 26 | 27 | {ruleSlice.map((r, i) => { 28 | return ; 29 | })} 30 | 31 | 37 | 38 | ); 39 | }; 40 | 41 | export default RuleList; 42 | -------------------------------------------------------------------------------- /src/components/home/rule_panel.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const rulePanelStyles = () => ({ 4 | item: css` 5 | min-width: 350px; 6 | min-height: 150px; 7 | `, 8 | badge: css` 9 | margin-top: 4px; 10 | `, 11 | link: css` 12 | text-align: center; 13 | `, 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/home/rule_panel.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { 3 | EuiBadge, 4 | EuiFlexItem, 5 | EuiPanel, 6 | EuiText, 7 | EuiSpacer, 8 | EuiLink, 9 | } from '@elastic/eui'; 10 | import Link from 'next/link'; 11 | 12 | import LazyLoad from 'react-lazy-load'; 13 | import { rulePanelStyles } from './rule_panel.styles'; 14 | import moment from 'moment'; 15 | 16 | import { RuleSummary } from '../../types'; 17 | import { ruleFilterTypeMap } from '../../lib/ruledata'; 18 | 19 | interface RulePanelProps { 20 | rule: RuleSummary; 21 | } 22 | 23 | const RulePanel: FunctionComponent = ({ children, rule }) => { 24 | const styles = rulePanelStyles(); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | {rule.name} 33 | 34 | 35 | 36 | 37 | <> 38 | 39 | {rule.tags 40 | .filter(t => !t.startsWith('Resources')) 41 | .map((t, i) => { 42 | const badgeTheme = ruleFilterTypeMap[t.split(': ')[0]] || { 43 | color: 'hollow', 44 | icon: '', 45 | }; 46 | return ( 47 | 52 | {t} 53 | 54 | ); 55 | })} 56 | 57 | 58 |

59 | Updated {moment(rule.updated_date).fromNow()} 60 |

61 |
62 | 63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default RulePanel; 70 | -------------------------------------------------------------------------------- /src/components/home/theme_switcher.styles.ts: -------------------------------------------------------------------------------- 1 | import { css, keyframes } from '@emotion/react'; 2 | 3 | const rotate = keyframes` 4 | 0% { 5 | transform: rotate(0); 6 | } 7 | 100% { 8 | transform: rotate(360deg); 9 | } 10 | `; 11 | 12 | export const themeSwitcherStyles = euiTheme => ({ 13 | animation: css` 14 | animation: ${rotate} 0.5s ease; 15 | transition: all ${euiTheme.animation.extraSlow} ${euiTheme.animation.bounce}; 16 | `, 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/home/theme_switcher.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import { 3 | EuiHeaderSectionItemButton, 4 | EuiIcon, 5 | EuiToolTip, 6 | useEuiTheme, 7 | } from '@elastic/eui'; 8 | import { useTheme } from '../theme'; 9 | import { themeSwitcherStyles } from './theme_switcher.styles'; 10 | 11 | const ThemeSwitcher: FunctionComponent = () => { 12 | const { colorMode, setColorMode } = useTheme(); 13 | const isDarkTheme = colorMode === 'dark'; 14 | 15 | const handleChangeTheme = (newTheme: string) => { 16 | setColorMode(newTheme); 17 | }; 18 | 19 | const lightOrDark = isDarkTheme ? 'light' : 'dark'; 20 | const { euiTheme } = useEuiTheme(); 21 | 22 | const styles = themeSwitcherStyles(euiTheme); 23 | 24 | return ( 25 | 26 | handleChangeTheme(lightOrDark)}> 29 | 35 | 36 | ); 37 | }; 38 | export default ThemeSwitcher; 39 | -------------------------------------------------------------------------------- /src/components/home/wrapper.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const wrapperStyles = euiTheme => ({ 4 | content: css` 5 | display: flex; 6 | flex-direction: column; 7 | margin: 0 auto; 8 | padding-right: ${euiTheme.size.base}; 9 | padding-bottom: ${euiTheme.size.xxl}; 10 | padding-left: ${euiTheme.size.base}; 11 | `, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/home/wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | import Header from './header'; 3 | import GradientBg from './gradient_bg'; 4 | import { useEuiTheme } from '@elastic/eui'; 5 | import { wrapperStyles } from './wrapper.styles'; 6 | 7 | const Wrapper: FunctionComponent = ({ children }) => { 8 | const { euiTheme } = useEuiTheme(); 9 | const styles = wrapperStyles(euiTheme); 10 | 11 | return ( 12 | <> 13 |
14 | 15 | 16 |
{children}
17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Wrapper; 23 | -------------------------------------------------------------------------------- /src/components/next_eui/button.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { EuiButton } from '@elastic/eui'; 3 | 4 | /** 5 | * Next's `` component passes a ref to its children, which triggers a warning 6 | * on EUI buttons (they expect `buttonRef`). Wrap the button component to pass on the 7 | * ref, and silence the warning. 8 | */ 9 | 10 | type EuiButtonProps = React.ComponentProps; 11 | const NextEuiButton = forwardRef< 12 | HTMLAnchorElement | HTMLButtonElement, 13 | EuiButtonProps 14 | >((props, ref) => { 15 | return ( 16 | // @ts-ignore EuiButton's ref is an HTMLButtonElement or an 17 | // HTMLAnchorElement, depending on whether `href` prop is passed 18 | 19 | {props.children} 20 | 21 | ); 22 | }); 23 | 24 | NextEuiButton.displayName = 'NextEuiButton(EuiButton)'; 25 | 26 | export default NextEuiButton; 27 | -------------------------------------------------------------------------------- /src/components/theme.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FunctionComponent, 3 | createContext, 4 | useContext, 5 | useState, 6 | useEffect, 7 | } from 'react'; 8 | import { getTheme, enableTheme } from '../lib/theme'; 9 | 10 | /** 11 | * React context for storing theme-related data and callbacks. 12 | * `colorMode` is `light` or `dark` and will be consumed by 13 | * various downstream components, including `EuiProvider`. 14 | */ 15 | export const GlobalProvider = createContext<{ 16 | colorMode?: string; 17 | setColorMode?: (colorMode: string) => void; 18 | }>({}); 19 | 20 | export const Theme: FunctionComponent = ({ children }) => { 21 | const [colorMode, setColorMode] = useState('light'); 22 | 23 | // on initial mount in the browser, use any theme from local storage 24 | useEffect(() => { 25 | setColorMode(getTheme()); 26 | }, []); 27 | 28 | // enable the correct theme when colorMode changes 29 | useEffect(() => enableTheme(colorMode), [colorMode]); 30 | 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | export const useTheme = () => { 39 | return useContext(GlobalProvider); 40 | }; 41 | -------------------------------------------------------------------------------- /src/custom_typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | // These type definitions allow us to import image files in JavaScript without 4 | // causing type errors. They tell the TypeScript compiler that such imports 5 | // simply return a value, which they do, thanks to Webpack. 6 | 7 | declare module '*.png' { 8 | const value: any; 9 | export = value; 10 | } 11 | 12 | declare module '*.svg' { 13 | const value: any; 14 | export = value; 15 | } 16 | 17 | // This section works with the `typescript-plugin-css-modules` plugin, and 18 | // allows us to type-check the name in our CSS modules (and get IDE completion!) 19 | declare module '*.module.scss' { 20 | const content: { [className: string]: string }; 21 | export default content; 22 | } 23 | -------------------------------------------------------------------------------- /src/images/logo_elastic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/detection-rules-explorer/fcf2960a95130fe9a3098b37920fe2a8e58b8119/src/images/logo_elastic.png -------------------------------------------------------------------------------- /src/lib/gtag.ts: -------------------------------------------------------------------------------- 1 | declare const window: any; 2 | export const GA_TRACKING_ID = 'G-7P2FQG4KX0'; 3 | 4 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages 5 | export const pageview = url => { 6 | window.gtag('config', GA_TRACKING_ID, { 7 | page_path: url, 8 | }); 9 | }; 10 | 11 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 12 | export const event = ({ action, category, label, value }) => { 13 | window.gtag('event', action, { 14 | event_category: category, 15 | event_label: label, 16 | value: value, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/loader.ts: -------------------------------------------------------------------------------- 1 | import { ImageLoader } from 'next/image'; 2 | 3 | export const imageLoader: ImageLoader = ({ src, width, quality }) => 4 | `${src}?w=${width}&q=${quality || 75}`; 5 | -------------------------------------------------------------------------------- /src/lib/ruledata.ts: -------------------------------------------------------------------------------- 1 | export const ruleFilterTypeMap = { 2 | Domain: { 3 | color: 'accent', 4 | icon: 'globe', 5 | }, 6 | 'Use Case': { 7 | color: 'primary', 8 | icon: 'launch', 9 | }, 10 | 'Data Source': { 11 | color: 'default', 12 | icon: 'database', 13 | }, 14 | 'Hunt Type': { 15 | color: 'default', 16 | icon: 'eye', 17 | }, 18 | OS: { 19 | color: 'success', 20 | icon: 'compute', 21 | }, 22 | Tactic: { 23 | color: 'warning', 24 | icon: 'bug', 25 | }, 26 | 'Rule Type': { 27 | color: 'hollow', 28 | icon: 'layers', 29 | }, 30 | Language: { 31 | color: 'default', 32 | icon: 'menu', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/theme.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The functions here are for tracking and setting the current theme. 3 | * localStorage is used to store the currently preferred them, though 4 | * that doesn't work on the server, where we just use a default. 5 | */ 6 | 7 | const selector = 'link[data-name="eui-theme"]'; 8 | export const defaultTheme = 'light'; 9 | 10 | function getAllThemes(): HTMLLinkElement[] { 11 | // @ts-ignore 12 | return [...document.querySelectorAll(selector)]; 13 | } 14 | 15 | export function enableTheme(newThemeName: string): void { 16 | const oldThemeName = getTheme(); 17 | localStorage.setItem('theme', newThemeName); 18 | 19 | for (const themeLink of getAllThemes()) { 20 | // Disable all theme links, except for the desired theme, which we enable 21 | themeLink.disabled = themeLink.dataset.theme !== newThemeName; 22 | themeLink['aria-disabled'] = themeLink.dataset.theme !== newThemeName; 23 | } 24 | 25 | // Add a class to the `body` element that indicates which theme we're using. 26 | // This allows any custom styling to adapt to the current theme. 27 | if (document.body.classList.contains(`appTheme-${oldThemeName}`)) { 28 | document.body.classList.replace( 29 | `appTheme-${oldThemeName}`, 30 | `appTheme-${newThemeName}` 31 | ); 32 | } else { 33 | document.body.classList.add(`appTheme-${newThemeName}`); 34 | } 35 | } 36 | 37 | export function getTheme(): string { 38 | const storedTheme = localStorage.getItem('theme'); 39 | 40 | return storedTheme || defaultTheme; 41 | } 42 | 43 | export interface Theme { 44 | id: string; 45 | name: string; 46 | publicPath: string; 47 | } 48 | 49 | // This is supplied to the app as JSON by Webpack - see next.config.js 50 | export interface ThemeConfig { 51 | availableThemes: Array; 52 | copyConfig: Array<{ 53 | from: string; 54 | to: string; 55 | }>; 56 | } 57 | 58 | // The config is generated during the build and made available in a JSON string. 59 | export const themeConfig: ThemeConfig = JSON.parse(process.env.THEME_CONFIG!); 60 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { 3 | EuiButton, 4 | EuiEmptyPrompt, 5 | EuiPageTemplate, 6 | EuiImage, 7 | } from '@elastic/eui'; 8 | import { useTheme } from '../components/theme'; 9 | import { useRouter } from 'next/router'; 10 | 11 | const NotFoundPage: FunctionComponent = () => { 12 | const { colorMode } = useTheme(); 13 | 14 | const isDarkTheme = colorMode === 'dark'; 15 | 16 | const illustration = isDarkTheme 17 | ? '/images/404_rainy_cloud_dark.png' 18 | : '/images/404_rainy_cloud_light.png'; 19 | 20 | const router = useRouter(); 21 | 22 | const handleClick = e => { 23 | e.preventDefault(); 24 | router.back(); 25 | }; 26 | 27 | return ( 28 | 29 | 30 | 37 | Go back 38 | , 39 | ]} 40 | body={ 41 |

42 | Sorry, we can't find the page you're looking for. It 43 | might have been removed or renamed, or maybe it never existed. 44 |

45 | } 46 | icon={} 47 | layout="vertical" 48 | title={

Page not found

} 49 | titleSize="m" 50 | /> 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default NotFoundPage; 57 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'core-js/stable'; 2 | import 'regenerator-runtime/runtime'; 3 | import { FunctionComponent } from 'react'; 4 | import { useEffect } from 'react'; 5 | import { AppProps } from 'next/app'; 6 | import { useRouter } from 'next/router'; 7 | import Head from 'next/head'; 8 | import { EuiErrorBoundary } from '@elastic/eui'; 9 | import { Global } from '@emotion/react'; 10 | import Chrome from '../components/chrome'; 11 | import { Theme } from '../components/theme'; 12 | import { globalStyes } from '../styles/global.styles'; 13 | import Script from 'next/script'; 14 | import * as gtag from '../lib/gtag'; 15 | 16 | /** 17 | * Next.js uses the App component to initialize pages. You can override it 18 | * and control the page initialization. Here use use it to render the 19 | * `Chrome` component on each page, and apply an error boundary. 20 | * 21 | * @see https://nextjs.org/docs/advanced-features/custom-app 22 | */ 23 | const EuiApp: FunctionComponent = ({ Component, pageProps }) => { 24 | const router = useRouter(); 25 | useEffect(() => { 26 | const handleRouteChange = url => { 27 | gtag.pageview(url); 28 | }; 29 | router.events.on('routeChangeComplete', handleRouteChange); 30 | return () => { 31 | router.events.off('routeChangeComplete', handleRouteChange); 32 | }; 33 | }, [router.events]); 34 | 35 | return ( 36 | <> 37 |