├── .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 |
45 |
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 |
34 |
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 |
41 |
55 |
56 | {/* You can override this in other pages - see index.tsx for an example */}
57 | Elastic Detection Rules Explorer
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | >
68 | );
69 | };
70 |
71 | export default EuiApp;
72 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import Document, { Head, Html, Main, NextScript } from 'next/document';
3 |
4 | import { defaultTheme, Theme, themeConfig } from '../lib/theme';
5 |
6 | const pathPrefix = process.env.PATH_PREFIX;
7 |
8 | function themeLink(theme: Theme): ReactElement {
9 | let disabledProps = {};
10 |
11 | if (theme.id !== defaultTheme) {
12 | disabledProps = {
13 | disabled: true,
14 | 'aria-disabled': true,
15 | };
16 | }
17 |
18 | return (
19 |
28 | );
29 | }
30 |
31 | /**
32 | * A custom `Document` is commonly used to augment your application's
33 | * `` and `` tags. This is necessary because Next.js pages skip
34 | * the definition of the surrounding document's markup.
35 | *
36 | * In this case, we customize the default `Document` implementation to
37 | * inject the available EUI theme files. Only the `light` theme is
38 | * initially enabled.
39 | *
40 | * @see https://nextjs.org/docs/advanced-features/custom-document
41 | */
42 | export default class MyDocument extends Document {
43 | render(): ReactElement {
44 | const isLocalDev = process.env.NODE_ENV === 'development';
45 |
46 | const favicon16Prod = `${pathPrefix}/images/favicon/prod/favicon-16x16.png`;
47 | const favicon32Prod = `${pathPrefix}/images/favicon/prod/favicon-32x32.png`;
48 | const favicon96Prod = `${pathPrefix}/images/favicon/prod/favicon-96x96.png`;
49 | const favicon16Dev = `${pathPrefix}/images/favicon/dev/favicon-16x16.png`;
50 | const favicon32Dev = `${pathPrefix}/images/favicon/dev/favicon-32x32.png`;
51 | const favicon96Dev = `${pathPrefix}/images/favicon/dev/favicon-96x96.png`;
52 |
53 | return (
54 |
55 |
56 |
60 |
61 |
65 |
69 |
70 | {themeConfig.availableThemes.map(each => themeLink(each))}
71 |
72 |
78 |
84 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/pages/_error.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react';
2 | import Error, { ErrorProps } from 'next/error';
3 |
4 | /**
5 | * An example of how to render a custom error page. Note that we have a
6 | * dedicated './404.tsx` page. See:
7 | *
8 | * https://nextjs.org/docs/advanced-features/custom-error-page
9 | */
10 | const ErrorWrapper: FunctionComponent = ({ statusCode }) => {
11 | return ;
12 | };
13 |
14 | // @ts-ignore getInitialProps doesn't exist on FunctionComponent
15 | ErrorWrapper.getInitialProps = ({ res, err }) => {
16 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
17 | return { statusCode };
18 | };
19 |
20 | export default ErrorWrapper;
21 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useState, useMemo } from 'react';
2 | import Head from 'next/head';
3 | import HomeHero from '../components/home/home_hero';
4 | import Wrapper from '../components/home/wrapper';
5 | import RuleList from '../components/home/rule_list';
6 |
7 | import newestRules from '../data/newestRules.json';
8 | import tagSummaries from '../data/tagSummaries.json';
9 |
10 | import { TagSummary } from '../types';
11 |
12 | const Index: FunctionComponent = () => {
13 | const [searchFilter, setSearchFilter] = useState('');
14 | const [tagFilter, setTagFilter] = useState([]);
15 |
16 | const rules = useMemo(() => {
17 | const newRules = newestRules.filter(function (r) {
18 | if (
19 | searchFilter &&
20 | !r.name.toLowerCase().includes(searchFilter.toLowerCase())
21 | ) {
22 | return false;
23 | }
24 | if (tagFilter.length > 0 && !tagFilter.every(t => r.tags.includes(t))) {
25 | return false;
26 | }
27 | return true;
28 | });
29 | return newRules;
30 | }, [searchFilter, tagFilter]);
31 |
32 | const filteredTagSummaries = useMemo(() => {
33 | const tagSummariesMap = new Map();
34 | for (const t of tagSummaries) {
35 | tagSummariesMap.set(t.tag_full, {
36 | tag_full: t.tag_full,
37 | tag_name: t.tag_name,
38 | tag_type: t.tag_type,
39 | count: 0,
40 | });
41 | }
42 | for (const r of rules) {
43 | for (const t of r.tags) {
44 | const parts = t.split(': ');
45 | const s = tagSummariesMap.get(t);
46 | if (parts.length != 2 || s == undefined) {
47 | continue;
48 | }
49 | s.count++;
50 | tagSummariesMap.set(t, s);
51 | }
52 | }
53 | return Array.from(tagSummariesMap.values());
54 | }, [rules]);
55 |
56 | const updateTagFilter = function (type: string, selected: string[]) {
57 | setTagFilter(tagFilter.filter(x => !x.startsWith(type)).concat(selected));
58 | };
59 |
60 | return (
61 | <>
62 |
63 | Home
64 |
65 |
66 |
67 |
75 |
76 |
77 | >
78 | );
79 | };
80 |
81 | export default Index;
82 |
--------------------------------------------------------------------------------
/src/pages/rules/[id].tsx:
--------------------------------------------------------------------------------
1 | import { InferGetStaticPropsType, GetStaticProps, GetStaticPaths } from 'next';
2 | import Head from 'next/head';
3 | import { EuiSpacer, useEuiTheme } from '@elastic/eui';
4 | import {
5 | EuiText,
6 | EuiDescriptionList,
7 | EuiFlexGroup,
8 | EuiFlexItem,
9 | EuiTitle,
10 | EuiBadge,
11 | EuiPanel,
12 | EuiCodeBlock,
13 | EuiLink,
14 | EuiHealth,
15 | EuiCallOut,
16 | } from '@elastic/eui';
17 | import moment from 'moment';
18 | import * as fs from 'fs';
19 | import * as path from 'path';
20 |
21 | import Wrapper from '../../components/home/wrapper';
22 | import { ruleDetailsStyles } from '../../components/details/rule_details.styles';
23 | import { ruleFilterTypeMap } from '../../lib/ruledata';
24 |
25 | const RULES_OUTPUT_PATH = '../../../../src/data/rules/';
26 |
27 | export const getStaticPaths: GetStaticPaths = async () => {
28 | const ids = fs.readdirSync(path.join(__dirname, RULES_OUTPUT_PATH));
29 | return {
30 | paths: ids.map(x => {
31 | console.log(path.parse(x).name);
32 | return {
33 | params: {
34 | id: path.parse(x).name,
35 | },
36 | };
37 | }),
38 | fallback: false,
39 | };
40 | };
41 |
42 | export const getStaticProps: GetStaticProps<{
43 | rule;
44 | }> = ({ params }) => {
45 | const res = JSON.parse(
46 | fs.readFileSync(
47 | path.join(__dirname, `${RULES_OUTPUT_PATH}${params.id}.json`),
48 | 'utf8'
49 | )
50 | );
51 | return { props: { rule: res } };
52 | };
53 |
54 | export default function RuleDetails({
55 | rule,
56 | }: InferGetStaticPropsType) {
57 | const { euiTheme } = useEuiTheme();
58 | const styles = ruleDetailsStyles(euiTheme);
59 |
60 | const ruleCreated =
61 | rule.metadata.creation_date &&
62 | moment(rule.metadata.creation_date.replace(/\//g, '-'));
63 | const ruleUpdated =
64 | rule.metadata.updated_date &&
65 | moment(rule.metadata.updated_date.replace(/\//g, '-'));
66 |
67 | const aboutItems = [
68 | {
69 | title: 'Tags',
70 | description: rule.rule.tags.map((t, i) => {
71 | if (t.startsWith('Resources')) {
72 | return <>>;
73 | }
74 | const badgeTheme = ruleFilterTypeMap[t.split(': ')[0]] || {
75 | color: 'hollow',
76 | icon: '',
77 | };
78 | return (
79 |
84 | {t}
85 |
86 | );
87 | }),
88 | },
89 | {
90 | title: 'Severity',
91 | description: (() => {
92 | let status = 'subdued';
93 | if (rule.rule.severity == 'medium') {
94 | status = 'warning';
95 | }
96 | if (rule.rule.severity == 'high') {
97 | status = 'danger';
98 | }
99 | return {rule.rule.severity} ;
100 | })(),
101 | },
102 | {
103 | title: 'Risk Score',
104 | description: rule.rule.risk_score,
105 | },
106 | {
107 | title: 'References',
108 | description:
109 | rule.rule.reference &&
110 | rule.rule.reference.map((x, i) => (
111 |
112 | {x}
113 |
114 | )),
115 | },
116 | {
117 | title: 'MITRE ATT&CK™',
118 | description:
119 | rule.rule.threat &&
120 | rule.rule.threat.map((x, i) => (
121 |
122 |
123 | {x.tactic.name} ({x.tactic.id})
124 |
125 | {x.technique &&
126 | x.technique.map((t, j) => (
127 |
128 |
129 |
130 | ↳ {t.name} ({t.id})
131 |
132 |
133 |
134 | ))}
135 |
136 |
137 | )),
138 | },
139 | {
140 | title: 'False Positive Examples',
141 | description: rule.rule.false_positives,
142 | },
143 | {
144 | title: 'License',
145 | description: (
146 |
149 | Elastic License v2
150 |
151 | ),
152 | },
153 | ].filter(x => x.description);
154 |
155 | const packName =
156 | rule.metadata.source_integration_name ||
157 | 'Prebuilt Security Detection Rules';
158 | const packLink = rule.metadata.source_integration
159 | ? `https://docs.elastic.co/en/integrations/${rule.metadata.source_integration}`
160 | : `https://www.elastic.co/guide/en/security/current/prebuilt-rules-management.html`;
161 |
162 | const definitionItems = [
163 | {
164 | title: 'Rule Type',
165 | description: (() => {
166 | switch (rule.rule.type) {
167 | case 'query':
168 | if (rule.rule.language == 'kuery') {
169 | return 'Query (Kibana Query Language)';
170 | }
171 | case 'eql':
172 | return 'Event Correlation Rule';
173 | case 'threshold':
174 | return 'Threshold Rule';
175 | case 'threat_match':
176 | return 'Threat Match Rule';
177 | case 'new_terms':
178 | return 'New Terms Rule';
179 | case 'machine_learning':
180 | return 'Machine Learning';
181 | }
182 | })(),
183 | },
184 | {
185 | title: 'Integration Pack',
186 | description: packName,
187 | },
188 | {
189 | title: 'Index Patterns',
190 | description:
191 | rule.rule.index &&
192 | rule.rule.index.map((x, i) => (
193 |
194 | {x}
195 |
196 | )),
197 | },
198 | {
199 | title: 'Related Integrations',
200 | description: [
201 | ...(Array.isArray(rule.metadata.integration)
202 | ? rule.metadata.integration
203 | : [rule.metadata.integration]),
204 | ].map((x, i) => (
205 |
206 |
209 | {x}
210 |
211 |
212 | )),
213 | },
214 | {
215 | title: 'Query',
216 | description: '',
217 | },
218 | ].filter(x => x.title == 'Query' || x.description);
219 |
220 | return (
221 | <>
222 |
223 | Home
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 | {rule.rule.name}
232 |
233 |
234 |
235 | Last updated {ruleUpdated.fromNow()} on{' '}
236 | {ruleUpdated.format('YYYY-MM-DD')}
237 |
238 | {ruleCreated && (
239 |
240 | Created {ruleCreated.fromNow()} on{' '}
241 | {ruleCreated.format('YYYY-MM-DD')}
242 |
243 | )}
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 | About
252 |
253 |
254 | {rule.rule.description}
255 |
256 |
261 |
262 |
263 |
264 |
265 |
266 | Definition
267 |
268 |
269 |
274 |
275 | {rule.rule.query}
276 |
277 |
278 |
279 |
284 |
285 |
286 | Detect {rule.rule.name} in the Elastic Security detection engine by
287 | installing this rule into your Elastic Stack.
288 |
289 |
290 | To setup this rule, check out the installation guide for{' '}
291 |
292 | {packName}
293 |
294 | .
295 |
296 |
297 |
298 | >
299 | );
300 | }
301 |
--------------------------------------------------------------------------------
/src/styles/getting-started.styles.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | export const gettingStartedStyles = euiTheme => ({
4 | wrapperInner: css`
5 | padding-top: ${euiTheme.size.xxl};
6 | padding-bottom: ${euiTheme.size.xl};
7 | `,
8 | });
9 |
--------------------------------------------------------------------------------
/src/styles/global.styles.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | export const globalStyes = css`
4 | #__next,
5 | .guideBody {
6 | min-height: 100%;
7 | display: flex;
8 | flex-direction: column;
9 | }
10 | `;
11 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface TagSummary {
2 | tag_type: string;
3 | tag_name: string;
4 | tag_full: string;
5 | count: number;
6 | }
7 |
8 | export interface RuleSummary {
9 | id: string;
10 | name: string;
11 | tags: Array;
12 | updated_date: string;
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "plugins": [
4 | {
5 | "name": "typescript-plugin-css-modules"
6 | }
7 | ],
8 | "target": "es5",
9 | "lib": [
10 | "dom",
11 | "dom.iterable",
12 | "esnext"
13 | ],
14 | "allowJs": true,
15 | "skipLibCheck": true,
16 | "strict": false,
17 | "forceConsistentCasingInFileNames": true,
18 | "noEmit": true,
19 | "esModuleInterop": true,
20 | "module": "esnext",
21 | "moduleResolution": "node",
22 | "resolveJsonModule": true,
23 | "isolatedModules": true,
24 | "jsx": "preserve",
25 | "jsxImportSource": "@emotion/react",
26 | "incremental": true
27 | },
28 | "include": [
29 | "./src/**/*"
30 | ],
31 | "exclude": [
32 | "node_modules"
33 | ]
34 | }
--------------------------------------------------------------------------------