chore: import upstream snapshot with attribution
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@redeye/client': patch
|
||||
---
|
||||
|
||||
Fix add campaign dialog description spacing
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": true,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "develop",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@redeye/models': patch
|
||||
---
|
||||
|
||||
Exposing BeaconMeta source LogLine
|
||||
@@ -0,0 +1,2 @@
|
||||
---
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
**/node_modules
|
||||
**/release
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
charset = utf-8
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"cypress/globals": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["import", "@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"no-debugger": "warn",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "separate-type-imports",
|
||||
"disallowTypeAnnotations": false
|
||||
}
|
||||
],
|
||||
"import/no-anonymous-default-export": ["error"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Build Check
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.16.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.16.1
|
||||
cache: 'yarn'
|
||||
- run: CYPRESS_INSTALL_BINARY=0 yarn install --immutable --inline-builds
|
||||
- run: CYPRESS_INSTALL_BINARY=0 yarn moon ci
|
||||
- uses: 'moonrepo/run-report-action@v1'
|
||||
if: success() || failure()
|
||||
with:
|
||||
access-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,33 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.16.1]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: |
|
||||
docker-compose -f docker-compose.yml up -d redeye-artifacts
|
||||
docker create -ti --name redeye-artifacts redeye-builder bash
|
||||
docker cp redeye-artifacts:/app/outputs ./artifacts
|
||||
docker rm -f redeye-artifacts
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: RedEye
|
||||
path: artifacts
|
||||
@@ -0,0 +1,238 @@
|
||||
name: Cypress Tests
|
||||
|
||||
on:
|
||||
# manual button click from the GitHub UI
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
uuid: ${{ steps.uuid.outputs.value }}
|
||||
steps:
|
||||
- name: Generate unique ID 💎
|
||||
id: uuid
|
||||
run: echo "{name}={sha-$GITHUB_SHA-time-$(date +"%s")}" >> $GITHUB_ENV
|
||||
|
||||
- name: Print unique ID 🖨`
|
||||
run: echo "generated id ${{ steps.uuid.outputs.value }}"
|
||||
|
||||
install:
|
||||
needs: ['prepare']
|
||||
container:
|
||||
image: cypress/included:cypress-12.17.3-node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
|
||||
options: --user 1001
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.16.1]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.16.1
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install --immutable --inline-builds
|
||||
- run: yarn moon run server:build client:build
|
||||
|
||||
- name: Save build folder
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: moon-cache
|
||||
path: .moon/cache
|
||||
if-no-files-found: error
|
||||
|
||||
RedTeamChrome:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: cypress/included:cypress-12.17.3-node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
|
||||
options: --user 1001
|
||||
needs: [prepare, install]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
containers: [1, 2]
|
||||
node-version: [18.16.1]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.16.1
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Download the build folders
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: moon-cache
|
||||
path: .moon/cache
|
||||
|
||||
- run: yarn install --immutable --inline-builds
|
||||
- name: Red Team Tests - Chrome v${{ matrix.node }}
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
tag: node-${{ matrix.node }}
|
||||
start: yarn start:test-app
|
||||
wait-on: 'http://localhost:4000/api/graphql, http://localhost:3500'
|
||||
browser: chrome
|
||||
group: 'Red Team - Chrome'
|
||||
config: baseUrl=http://localhost:3500
|
||||
record: true
|
||||
parallel: true
|
||||
config-file: applications/redeye-e2e/cypress.config.js
|
||||
spec: applications/redeye-e2e/src/integration/e2e/redteam/**/**/**/*.cy.js
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
env:
|
||||
CYPRESS_PROJECT_ID: 'rsybgk'
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY_RED }}
|
||||
|
||||
BlueTeamChrome:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: cypress/included:cypress-12.17.3-node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
|
||||
options: --user 1001
|
||||
needs: [prepare, install]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
containers: [1, 2]
|
||||
node-version: [18.16.1]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.16.1
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Download the build folders
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: moon-cache
|
||||
path: .moon/cache
|
||||
|
||||
- run: yarn install --immutable --inline-builds
|
||||
- name: 'Blue Team Tests - Chrome'
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
tag: node-${{ matrix.node }}
|
||||
start: yarn start:test-app
|
||||
wait-on: 'http://localhost:4000/api/graphql, http://localhost:3500'
|
||||
browser: chrome
|
||||
group: 'Blue Team - Chrome'
|
||||
config: baseUrl=http://localhost:3500
|
||||
record: true
|
||||
parallel: true
|
||||
config-file: applications/redeye-e2e/cypress.config.js
|
||||
spec: applications/redeye-e2e/src/integration/e2e/blueteam/**/*.cy.js
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
env:
|
||||
CYPRESS_PROJECT_ID: '46ahz3'
|
||||
SERVER_BLUE_TEAM: 'true'
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY_BLUE }}
|
||||
|
||||
# RedTeamFirefox:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [prepare, install]
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
#
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v3
|
||||
#
|
||||
# - name: Cache
|
||||
# uses: actions/setup-node@v3
|
||||
# with:
|
||||
# cache: 'yarn'
|
||||
# - run: git fetch
|
||||
# - run: git config pull.rebase true
|
||||
# - run: git pull origin $GITHUB_HEAD_REF
|
||||
# - run: yarn install
|
||||
# - run: yarn moon run server:build && yarn moon run client:build
|
||||
#
|
||||
# - name: 'Red Team Tests - Firefox'
|
||||
# uses: cypress-io/github-action@v5.0.0
|
||||
# with:
|
||||
# start: yarn start
|
||||
# wait-on: 'http://localhost:4000/api/graphql, http://localhost:3500'
|
||||
# browser: firefox
|
||||
# group: 'Red Team - Firefox'
|
||||
# working-directory: applications/redeye-e2e
|
||||
# config: baseUrl=http://localhost:3500
|
||||
# record: true
|
||||
# parallel: true
|
||||
# config-file: cypress.config.js
|
||||
# spec: src/integration/e2e/redteam/**/**/**/*.cy.js
|
||||
# ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
# env:
|
||||
# CYPRESS_PROJECT_ID: 'rsybgk'
|
||||
# CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY_RED1 }}
|
||||
#
|
||||
# BlueTeamFirefox:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [prepare, install]
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
#
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v3
|
||||
#
|
||||
# - name: Download the build folders
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: moon-cache
|
||||
# path: .moon/cache
|
||||
#
|
||||
# - name: Cache
|
||||
# uses: actions/setup-node@v3
|
||||
# with:
|
||||
# cache: 'yarn'
|
||||
# - run: git fetch
|
||||
# - run: git config pull.rebase true
|
||||
# - run: git pull origin $GITHUB_HEAD_REF
|
||||
# - run: yarn install
|
||||
# - run: yarn moon run server:build && yarn moon run client:build
|
||||
#
|
||||
# - name: 'Blue Team Tests - Firefox'
|
||||
# uses: cypress-io/github-action@v5.0.0
|
||||
# with:
|
||||
# start: yarn start:blue
|
||||
# wait-on: 'http://localhost:4000/api/graphql, http://localhost:3500'
|
||||
# browser: firefox
|
||||
# group: 'Blue Team - Firefox'
|
||||
# working-directory: applications/redeye-e2e
|
||||
# config: baseUrl=http://localhost:3500
|
||||
# record: true
|
||||
# parallel: true
|
||||
# config-file: cypress.config.js
|
||||
# spec: src/integration/e2e/blueteam/**/*.cy.js
|
||||
# ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
# env:
|
||||
# CYPRESS_PROJECT_ID: '46ahz3'
|
||||
# CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY_BLUE1 }}
|
||||
@@ -0,0 +1,46 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
*.local
|
||||
dist
|
||||
build
|
||||
@redeye/darwin-x64
|
||||
packages/graph/testData
|
||||
applications/server/dev-databases
|
||||
applications/server/campaign
|
||||
.eslintcache
|
||||
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
coverage
|
||||
junit.xml
|
||||
|
||||
.history
|
||||
.idea
|
||||
.vim
|
||||
.nova
|
||||
|
||||
[diff]
|
||||
tool = vscode
|
||||
[difftool "vscode"]
|
||||
cmd = code --wait --diff $LOCAL $REMOTE
|
||||
|
||||
test.sqlite*
|
||||
|
||||
junit.*.xml
|
||||
release
|
||||
tmp
|
||||
# moon
|
||||
.moon/cache
|
||||
.moon/docker
|
||||
*.tsbuildinfo
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged --relative
|
||||
@@ -0,0 +1,347 @@
|
||||
# https://moonrepo.dev/docs/config/global-project
|
||||
$schema: 'https://moonrepo.dev/schemas/global-project.json'
|
||||
|
||||
# Extend and inherit an external configuration file. Must be a valid HTTPS URL or file system path.
|
||||
# extends: './shared/project.yml'
|
||||
|
||||
# File groups are a mechanism for grouping similar types of files within a project
|
||||
# using file glob patterns. These groups are then used by tasks to calculate functionality like
|
||||
# cache computation, affected files since last change, command line arguments, deterministic
|
||||
# builds, and more.
|
||||
#
|
||||
# This setting requires a map, where the key is the file group name, and the value is a list of
|
||||
# globs. Globs are relative to a project, even though they are defined globally. This enables
|
||||
# enforcement of organizational patterns across all projects in the workspace.
|
||||
fileGroups:
|
||||
# List of configuration files located within the project root.
|
||||
configs:
|
||||
- '*.{js,json,yml,yaml}'
|
||||
|
||||
# List of non-test source files.
|
||||
sources:
|
||||
- 'src/**/*'
|
||||
- 'types/**/*'
|
||||
- 'package.json'
|
||||
|
||||
# List of non-source test files.
|
||||
tests:
|
||||
- 'tests/**/*.test.*'
|
||||
- '**/__tests__/**/*'
|
||||
|
||||
# All static assets within the project.
|
||||
# This may include styles, images, videos, etc.
|
||||
assets:
|
||||
- 'assets/**/*'
|
||||
- 'images/**/*'
|
||||
- 'static/**/*'
|
||||
- '**/*.{scss,css}'
|
||||
- '**/*.mdx'
|
||||
|
||||
# A task is an action that is ran within the context of a project, and wraps
|
||||
# around an npm or system command. Tasks that are defined here and inherited by all projects
|
||||
# within the workspace, but can be overridden per project.
|
||||
#
|
||||
# This setting requires a map, where the key is a unique name for the task,
|
||||
# and the value is an object of task parameters.
|
||||
tasks:
|
||||
xstate-typegen:
|
||||
command:
|
||||
- xstate
|
||||
- typegen
|
||||
- src/**/*.machine.ts?(x)
|
||||
outputs:
|
||||
- 'src/**/*.machine.typegen.{ts,tsx}'
|
||||
inputs:
|
||||
- 'src/**/*.machine.{ts,tsx}'
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
test-jest:
|
||||
command: 'jest . --coverage --reporters="jest-junit" --passWithNoTests'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(tests)'
|
||||
- 'jest.config.ts'
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
lint-ci:
|
||||
command: 'eslint'
|
||||
options:
|
||||
affectedFiles: true
|
||||
runInCI: false
|
||||
lint:
|
||||
command: 'eslint src --fix --max-warnings 0'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(tests)'
|
||||
- '.eslintignore'
|
||||
- '.eslintrc.yaml'
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
lint-global:
|
||||
command: 'eslint --config $workspaceRoot/.eslintrc.json --ignore-path $workspaceRoot/.gitignore src --fix'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(tests)'
|
||||
- '.eslintignore'
|
||||
- '.eslintrc.json'
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
format:
|
||||
command:
|
||||
- prettier
|
||||
- '**/*.{ts,tsx,js,jsx,json,yml,yaml,astro}'
|
||||
- '!**/*.graphql'
|
||||
- --write
|
||||
- --config
|
||||
- $workspaceRoot/.prettierrc.yaml
|
||||
- --ignore-unknown
|
||||
- --ignore-path
|
||||
- $workspaceRoot/.gitignore
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
options:
|
||||
runInCI: false
|
||||
affectedFiles: true
|
||||
platform: node
|
||||
|
||||
# Vite commands
|
||||
|
||||
start-vite:
|
||||
command: 'vite'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(assets)'
|
||||
- 'vite.config.ts'
|
||||
local: true
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
build-vite:
|
||||
command: 'vite build'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(assets)'
|
||||
- 'vite.config.ts'
|
||||
- 'tsconfig.json'
|
||||
outputs:
|
||||
- dist
|
||||
- tsconfig.tsbuildinfo
|
||||
env:
|
||||
NODE_ENV: production
|
||||
deps:
|
||||
- ~:clean-dist
|
||||
- ^:build
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
runDepsInParallel: false
|
||||
preview-vite:
|
||||
command: 'vite preview --port 3500'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(assets)'
|
||||
- 'vite.config.ts'
|
||||
local: true
|
||||
platform: node
|
||||
deps:
|
||||
- ~:build-vite
|
||||
options:
|
||||
runInCI: false
|
||||
|
||||
# Library mode commands
|
||||
build-library:
|
||||
command: 'noop'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(tests)'
|
||||
- 'tsconfig.json'
|
||||
outputs:
|
||||
- dist
|
||||
- 'tsconfig.tsbuildinfo'
|
||||
deps:
|
||||
- ^:build
|
||||
- ~:build-client-library
|
||||
- ~:typecheck
|
||||
env:
|
||||
NODE_ENV: production
|
||||
platform: node
|
||||
options:
|
||||
runDepsInParallel: false
|
||||
runInCI: false
|
||||
build-client-library:
|
||||
command: 'tsc --project tsconfig.json --module esnext --sourceMap --outDir @out(0)'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(assets)'
|
||||
- 'tsconfig.json'
|
||||
outputs:
|
||||
- dist
|
||||
env:
|
||||
NODE_ENV: production
|
||||
deps:
|
||||
- ~:clean-dist
|
||||
platform: node
|
||||
options:
|
||||
runDepsInParallel: false
|
||||
runInCI: false
|
||||
watch-library:
|
||||
command: 'tsc-watch --project tsconfig.json --module esnext --sourceMap --outDir @out(0)'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(assets)'
|
||||
outputs:
|
||||
- dist
|
||||
env:
|
||||
NODE_ENV: production
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
cache: false
|
||||
|
||||
# Node commands
|
||||
build-node-library:
|
||||
command: 'tsc --project tsconfig.json --module commonjs --sourceMap --outDir @out(0)'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(assets)'
|
||||
- 'tsconfig.json'
|
||||
outputs:
|
||||
- dist
|
||||
env:
|
||||
NODE_ENV: production
|
||||
deps:
|
||||
- ~:clean-dist
|
||||
platform: node
|
||||
options:
|
||||
runDepsInParallel: false
|
||||
runInCI: false
|
||||
build-node:
|
||||
command: 'noop'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(tests)'
|
||||
- 'tsconfig.json'
|
||||
deps:
|
||||
- ^:build
|
||||
- ~:build-node-library
|
||||
env:
|
||||
NODE_ENV: production
|
||||
platform: node
|
||||
options:
|
||||
runDepsInParallel: false
|
||||
runInCI: false
|
||||
|
||||
start-node:
|
||||
env:
|
||||
NODE_ENV: development
|
||||
command: 'tsc-watch --project tsconfig.json --module commonjs --sourceMap --outDir dist --onSuccess "yarn node --enable-source-maps dist"'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(tests)'
|
||||
- 'tsconfig.json'
|
||||
platform: node
|
||||
local: true
|
||||
options:
|
||||
runInCI: false
|
||||
|
||||
# Release Commands
|
||||
release-all:
|
||||
command: 'noop'
|
||||
deps:
|
||||
- ~:release-linux
|
||||
- ~:release-windows
|
||||
- ~:release-mac
|
||||
platform: system
|
||||
options:
|
||||
mergeOutputs: 'replace'
|
||||
runInCI: false
|
||||
release-mac:
|
||||
command: 'yarn pkg $projectRoot/package.json -t node18-mac -o @out(0)'
|
||||
deps:
|
||||
- ~:fetch-pkg-mac
|
||||
- ~:build
|
||||
platform: system
|
||||
outputs:
|
||||
- /release/mac/RedEye
|
||||
options:
|
||||
mergeOutputs: 'replace'
|
||||
runInCI: false
|
||||
runFromWorkspaceRoot: true
|
||||
release-linux:
|
||||
command: 'yarn pkg $projectRoot/package.json -t node18-linux -o @out(0)'
|
||||
deps:
|
||||
- ~:fetch-pkg-linux
|
||||
- ~:build
|
||||
platform: system
|
||||
outputs:
|
||||
- /release/linux/RedEye
|
||||
options:
|
||||
mergeOutputs: 'replace'
|
||||
runInCI: false
|
||||
runFromWorkspaceRoot: true
|
||||
release-windows:
|
||||
command: 'yarn pkg $projectRoot/package.json -t node18-win -o @out(0)'
|
||||
deps:
|
||||
- ~:fetch-pkg-windows
|
||||
- ~:build
|
||||
outputs:
|
||||
- /release/windows/RedEye
|
||||
options:
|
||||
mergeOutputs: 'replace'
|
||||
runInCI: false
|
||||
runFromWorkspaceRoot: true
|
||||
platform: node
|
||||
|
||||
## Fetch pkg commands
|
||||
fetch-pkg-mac:
|
||||
command: 'yarn pkg-fetch --platform mac --node-range node18'
|
||||
platform: system
|
||||
options:
|
||||
runInCI: false
|
||||
retryCount: 2
|
||||
runFromWorkspaceRoot: true
|
||||
fetch-pkg-linux:
|
||||
command: 'yarn pkg-fetch --platform linux --node-range node18'
|
||||
platform: system
|
||||
options:
|
||||
runInCI: false
|
||||
retryCount: 2
|
||||
runFromWorkspaceRoot: true
|
||||
fetch-pkg-windows:
|
||||
command: 'yarn pkg-fetch --platform win --node-range node18'
|
||||
platform: system
|
||||
options:
|
||||
runInCI: false
|
||||
retryCount: 2
|
||||
runFromWorkspaceRoot: true
|
||||
|
||||
## Clean
|
||||
clean-dist:
|
||||
command: 'rimraf ./dist && rimraf tsconfig.tsbuildinfo'
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
cache: false
|
||||
|
||||
## Typecheck
|
||||
typecheck:
|
||||
command: 'tsc --emitDeclarationOnly --outDir @out(0)'
|
||||
inputs:
|
||||
- '@globs(sources)'
|
||||
- '@globs(tests)'
|
||||
- 'tsconfig.json'
|
||||
outputs:
|
||||
- dist
|
||||
- 'tsconfig.tsbuildinfo'
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
noop:
|
||||
command: 'noop'
|
||||
platform: node
|
||||
options:
|
||||
runInCI: false
|
||||
@@ -0,0 +1,72 @@
|
||||
# https://moonrepo.dev/docs/config/toolchain
|
||||
$schema: 'https://moonrepo.dev/schemas/toolchain.json'
|
||||
|
||||
# Extend and inherit an external configuration file. Must be a valid HTTPS URL or file system path.
|
||||
# extends: './shared/toolchain.yml'
|
||||
|
||||
# Configures Node.js within the toolchain. moon manages its own version of Node.js
|
||||
# instead of relying on a version found on the host machine. This ensures deterministic
|
||||
# and reproducible builds across any machine.
|
||||
node:
|
||||
# The version to use. Must be a semantic version that includes major, minor, and patch.
|
||||
# We suggest using the latest active LTS version: https://nodejs.org/en/about/releases
|
||||
version: '18.16.1'
|
||||
|
||||
# The package manager to use when managing dependencies.
|
||||
# Accepts "npm" (default), "pnpm", or "yarn".
|
||||
packageManager: 'yarn'
|
||||
|
||||
# The version of the package manager (above) to use.
|
||||
yarn:
|
||||
version: '3.5.1'
|
||||
|
||||
# Add `node.version` as a constraint in the root `package.json` `engines`.
|
||||
addEnginesConstraint: true
|
||||
|
||||
# Use the `package.json` name as an alias for the respective moon project.
|
||||
aliasPackageNames: 'name-and-scope'
|
||||
|
||||
# Dedupe dependencies after the lockfile has changed.
|
||||
dedupeOnLockfileChange: true
|
||||
|
||||
# Version format to use when syncing dependencies within the project's `package.json`.
|
||||
# dependencyVersionFormat: 'workspace'
|
||||
|
||||
# Infer and automatically create moon tasks from `package.json` scripts, per project.
|
||||
# BEWARE: Tasks and scripts are not 1:1 in functionality, so please refer to the documentation.
|
||||
inferTasksFromScripts: false
|
||||
|
||||
# Sync a project's `dependsOn` as dependencies within the project's `package.json`.
|
||||
syncProjectWorkspaceDependencies: true
|
||||
|
||||
# Sync `node.version` to a 3rd-party version manager's config file.
|
||||
# Accepts "nodenv" (.node-version), "nvm" (.nvmrc), or none.
|
||||
syncVersionManagerConfig: 'nvm'
|
||||
|
||||
# Configures how moon integrates with TypeScript.
|
||||
typescript:
|
||||
# When `syncProjectReferences` is enabled and a dependent project reference
|
||||
# *does not* have a `tsconfig.json`, automatically create one.
|
||||
createMissingConfig: false
|
||||
|
||||
# Name of `tsconfig.json` file in each project root.
|
||||
# projectConfigFileName: 'tsconfig.json'
|
||||
|
||||
# Name of `tsconfig.json` file in the workspace root.
|
||||
# rootConfigFileName: 'tsconfig.json'
|
||||
|
||||
# Name of the config file in the workspace root that defines shared compiler
|
||||
# options for all project reference based config files.
|
||||
# rootOptionsConfigFileName: 'tsconfig.options.json'
|
||||
|
||||
# Update a project's `tsconfig.json` to route the `outDir` compiler option
|
||||
# to moon's `.moon/cache` directory.
|
||||
routeOutDirToCache: false
|
||||
|
||||
# Sync a project's `dependsOn` as project references within the
|
||||
# project's `tsconfig.json` and the workspace root `tsconfig.json`.
|
||||
syncProjectReferences: true
|
||||
|
||||
# Sync a project's project references as import aliases to the `paths`
|
||||
# compiler option in each applicable project.
|
||||
syncProjectReferencesToPaths: true
|
||||
@@ -0,0 +1,35 @@
|
||||
# https://moonrepo.dev/docs/config/workspace
|
||||
$schema: 'https://moonrepo.dev/schemas/workspace.json'
|
||||
|
||||
# Require a specific version of moon while running commands, otherwise fail.
|
||||
# versionConstraint: '>=0.0.0'
|
||||
|
||||
# Extend and inherit an external configuration file. Must be a valid HTTPS URL or file system path.
|
||||
# extends: './shared/workspace.yml'
|
||||
# runner:
|
||||
# implicitDeps:
|
||||
# - '^:build'
|
||||
# REQUIRED: A map of all projects found within the workspace, or a list or file system globs.
|
||||
# When using a map, each entry requires a unique project ID as the map key, and a file system
|
||||
# path to the project folder as the map value. File paths are relative from the workspace root,
|
||||
# and cannot reference projects located outside the workspace boundary.
|
||||
projects:
|
||||
globs:
|
||||
- 'applications/*'
|
||||
- 'packages/*'
|
||||
- 'parsers/*'
|
||||
sources:
|
||||
landing-page: 'docs/landing-page'
|
||||
|
||||
# Configures the version control system to utilize within the workspace. A VCS
|
||||
# is required for determining touched (added, modified, etc) files, calculating file hashes,
|
||||
# computing affected files, and much more.
|
||||
vcs:
|
||||
# The manager/binary to use when managing the repository.
|
||||
# Accepts "git", or "svn". Defaults to "git".
|
||||
manager: 'git'
|
||||
|
||||
# The default branch (master/main/trunk) in the repository for comparing the
|
||||
# local branch against. For git, this is is typically "master" or "main",
|
||||
# and must include the remote prefix (before /). For svn, this should always be "trunk".
|
||||
defaultBranch: 'develop'
|
||||
@@ -0,0 +1,7 @@
|
||||
printWidth: 120
|
||||
semi: true
|
||||
singleQuote: true
|
||||
trailingComma: 'es5'
|
||||
endOfLine: 'lf'
|
||||
tabWidth: 2
|
||||
useTabs: true
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"arcanis.vscode-zipfs",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"firsttris.vscode-jest-runner"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"RedEye React Function": {
|
||||
"prefix": "rfc",
|
||||
"body": [
|
||||
"import { css } from '@emotion/react';",
|
||||
"import { observer } from 'mobx-react-lite';",
|
||||
"import type { ComponentProps } from 'react';",
|
||||
"",
|
||||
"type ${TM_FILENAME_BASE/(^.)/${1:/upcase}/}Props = ComponentProps<'div'> & {}",
|
||||
"",
|
||||
"export const ${TM_FILENAME_BASE/(^.)/${1:/upcase}/} = observer<${TM_FILENAME_BASE/(^.)/${1:/upcase}/}Props>(({ ...props }) => {",
|
||||
" return <div cy-test=\"${TM_FILENAME_BASE/(^.)/${1:/downcase}/}-root\" {...props} css={css``}></div>;",
|
||||
"});",
|
||||
""
|
||||
],
|
||||
"description": "RedEye React Function"
|
||||
},
|
||||
"RedEye React Function With State": {
|
||||
"prefix": "rsfc",
|
||||
"body": [
|
||||
"import { css } from '@emotion/react';",
|
||||
"import { observer, useLocalObservable } from 'mobx-react-lite';",
|
||||
"import type { ComponentProps } from 'react';",
|
||||
"import { useStore } from '~/store';",
|
||||
"",
|
||||
"type ${TM_FILENAME_BASE/(^.)/${1:/upcase}/}Props = ComponentProps<'div'> & {}",
|
||||
"",
|
||||
"export const ${TM_FILENAME_BASE/(^.)/${1:/upcase}/}: observer<${TM_FILENAME_BASE/(^.)/${1:/upcase}/}Props>(({ ...props }) => {",
|
||||
" const store = useStore();",
|
||||
" const state = useLocalObservable(() => ({}));",
|
||||
" return <div cy-test=\"${TM_FILENAME_BASE/(^.)/${1:/downcase}/}-root\" {...props} css={css``}></div>;",
|
||||
"});",
|
||||
""
|
||||
],
|
||||
"description": "RedEye Stateful React Function"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"**/.yarn": true,
|
||||
"**/yarn.lock": true,
|
||||
"**/.pnp.*": true,
|
||||
"**/*.drawio": true
|
||||
},
|
||||
"eslint.nodePath": ".yarn/sdks",
|
||||
"prettier.prettierPath": "./node_modules/prettier",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"cSpell.words": ["Checkin", "deconflict", "subprocesses", "xstate"],
|
||||
"yaml.customTags": ["!configurable mapping", "!secret mapping"],
|
||||
"[typescript][typescriptreact][javascript][javascriptreact][json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"cypressHelper.customCommandsFolder": "applications/redeye-e2e/src/support",
|
||||
|
||||
"cypressHelper.commandForOpen": "yarn cypress open",
|
||||
|
||||
"cypressHelper.commandForRun": "yarn cypress run"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
enableTelemetry: false
|
||||
|
||||
logFilters:
|
||||
- code: YN0013
|
||||
level: discard
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@ArTs/plugin-http-proxy.cjs
|
||||
spec: 'https://github.com/arontsang/yarn-plugin-http-proxy/releases/latest/download/index.cjs'
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.5.1.cjs
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when
|
||||
an individual is representing the project or its community in public spaces.
|
||||
Examples of representing a project or community include using an official
|
||||
project e-mail address, posting via an official social media account, or acting
|
||||
as an appointed representative at an online or offline event. Representation of
|
||||
a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -0,0 +1,23 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for investing your time in contributing to our project!
|
||||
|
||||
Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable.
|
||||
|
||||
## New contributor guide
|
||||
|
||||
To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions:
|
||||
|
||||
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
|
||||
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
|
||||
- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
|
||||
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
|
||||
|
||||
## Getting started
|
||||
|
||||
1. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) and [clone](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) the [repository](https://github.com/cisa/redeye)
|
||||
2. [Create a separate branch](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-branches) for your changes
|
||||
3. Make your changes, and ensure that it is formatted by [Prettier](https://prettier.io) and lint-checks without errors in [ESLint](https://eslint.org)
|
||||
4. Create a changeset by running `yarn changeset`. [More info](https://github.com/atlassian/changesets).
|
||||
5. You can run tests locally to validate changes with `yarn run test`. [More info](https://www.cypress.io).
|
||||
6. Push your branch and open a PR 🚀
|
||||
@@ -0,0 +1,19 @@
|
||||
Disclaimer
|
||||
|
||||
|
||||
|
||||
This material was prepared as an account of work sponsored by an agency of the United States Government. Neither the United States Government nor the United States Department of Energy, nor Battelle, nor any of their employees, nor any jurisdiction or organization that has cooperated in the development of these materials, makes any warranty, express or implied, or assumes any legal liability or responsibility for the accuracy, completeness, or usefulness or any information, apparatus, product, software, or process disclosed, or represents that its use would not infringe privately owned rights.
|
||||
|
||||
Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or any agency thereof, or Battelle Memorial Institute. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or any agency thereof.
|
||||
|
||||
PACIFIC NORTHWEST NATIONAL LABORATORY
|
||||
|
||||
operated by
|
||||
|
||||
BATTELLE
|
||||
|
||||
for the
|
||||
|
||||
UNITED STATES DEPARTMENT OF ENERGY
|
||||
|
||||
under Contract DE-AC05-76RL01830
|
||||
@@ -0,0 +1,28 @@
|
||||
FROM node:18-bullseye as redeye-builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./ ./
|
||||
ENV CYPRESS_INSTALL_BINARY=0
|
||||
RUN npm install -g pkg rimraf
|
||||
RUN yarn install --immutable --inline-builds
|
||||
RUN yarn moon run server:build client:build
|
||||
RUN bash create-release.sh
|
||||
RUN tar -zcvf release.tar.gz ./release/
|
||||
RUN mkdir outputs
|
||||
RUN cp release.tar.gz outputs/release.tar.gz
|
||||
|
||||
FROM node:18-bullseye as redeye-linux-builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./ ./
|
||||
ENV CYPRESS_INSTALL_BINARY=0
|
||||
RUN yarn install --immutable --inline-builds
|
||||
RUN npx pkg-fetch --platform linux --node-range node18
|
||||
RUN yarn release:linux
|
||||
|
||||
### CORE IMAGE ###
|
||||
FROM debian as redeye-core
|
||||
WORKDIR /app
|
||||
COPY --from=redeye-linux-builder /app/release/linux .
|
||||
ENTRYPOINT [ "/bin/bash", "-l", "-c" ]
|
||||
CMD ["./RedEye"]
|
||||
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright © 2022 Battelle Memorial Institute
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,228 @@
|
||||
# **RedEye**: Red Team C2 Log Visualization
|
||||
---
|
||||
|
||||
## ⚠️ This Repo Currently in Maintenance Mode ⚠️
|
||||
|
||||
This GitHub repository is no longer under active development. We'll review community issues and pull requests for bug fixes, but won't consider any new feature additions.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img alt="RedEye Screenshot" src="https://github.com/cisagov/RedEye/blob/develop/docs/images/RedEye-Hero-Screenshot.png?raw=true" width="100%"/>
|
||||
</p>
|
||||
|
||||
RedEye is an open-source analytic tool developed by [CISA](https://www.cisa.gov/) and [DOE](https://www.energy.gov/)’s [Pacific Northwest National Laboratory](https://www.pnnl.gov/) to assist [Red Teams](https://en.wikipedia.org/wiki/Red_team) with visualizing and reporting command and control activities. This tool allows an operator to assess and display complex data, evaluate mitigation strategies, and enable effective decision making in response to a Red Team assessment. The tool parses logs, such as those from [Cobalt Strike](https://www.cobaltstrike.com/), and presents the data in an easily digestible format. The users can then tag and add comments to activities displayed within the tool. The operators can use the RedEye’s presentation mode to present findings and workflow to stakeholders.
|
||||
|
||||
RedEye can assist an operator to efficiently:
|
||||
|
||||
- Replay and demonstrate Red Team’s assessment activities as they occurred rather than manually pouring through thousands of lines of log text.
|
||||
- Display and evaluate complex assessment data to enable effective decision making.
|
||||
- Gain a clearer understanding of the attack path taken and the hosts compromised during a Red Team assessment or penetration test.
|
||||
|
||||
| **Red Team** | **Blue Team** |
|
||||
|:------------:|:-------------:|
|
||||
| [](https://cloud.cypress.io/projects/rsybgk/runs) | [](https://cloud.cypress.io/projects/46ahz3/runs) |
|
||||
|
||||
|
||||
## Quick start
|
||||
|
||||
1. **Download** the latest RedEye binaries for your OS[\*](#platform-support) from the [Releases](https://github.com/cisagov/RedEye/releases) page.
|
||||
2. **Pick a mode** and **Run the server**
|
||||
- [ **Red Team mode**](#red-team) enables the full feature set: upload C2 logs, explore data, and create presentations. _You must provide a password to run in Red Team mode._ To start the server in Red Team mode, run the following in a terminal.
|
||||
```
|
||||
./RedEye --redTeam --password <your_password>
|
||||
```
|
||||
- [**Blue Team mode**](#blue-team) (default) enables a simplified, read-only UI for reviewing campaigns exported by a Red Team. To start the server in Blue Team mode, run the following in a terminal.
|
||||
```
|
||||
./RedEye # Or simplify double-click the "RedEye" executable
|
||||
```
|
||||
3. **Use the web app** in a browser at http://127.0.0.1:4000. The RedEye binary runs as a server in a terminal window and will automatically open the web app UI your default browser. You must close the terminal window to quit the RedEye server.
|
||||
4. Try importing the [gt.redeye](https://github.com/cisagov/RedEye/raw/develop/applications/redeye-e2e/src/fixtures/gt.redeye) example dataset to get started. Or try a different [example dataset](#example-datasets).
|
||||
|
||||
_**MacOS Issue** - When running RedEye for the first time, you may get a "not verified" error. You must go to "System Preferences" > "Security & Privacy" > "General" and click "Open Anyway." More info on the [Apple support page](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/)._
|
||||
|
||||
### Follow the [User Guide](https://github.com/cisagov/RedEye/blob/develop/docs/UserGuide.md) to learn about RedEye's feature set.
|
||||
|
||||
---
|
||||
|
||||
## Red Team & Blue Team Modes
|
||||
|
||||
RedEye has two modes that cover two stages of the Red Teaming process. [Red Team mode](#red-team) allows importing C2 data, editing imported data, and making comments and presentations. After curating and annotating campaign data, Red Teams can export their campaign as a standalone `.redeye` file and [hand it off to a Blue Team](#blue-team-presentation-handoff) for reporting and remediation. [Blue Team mode](#blue-team) runs RedEye in a simplified read-only mode for viewing curated data exported by a Red Team.
|
||||
|
||||
_Note: Both Red and Blue Team modes can be started from the same RedEye application binary._
|
||||
|
||||
### Red Team
|
||||
|
||||
The downloaded binary comes in two parts:
|
||||
|
||||
- The `RedEye` application binary
|
||||
- The `parsers` folder containing parser binaries (e.g. `cobalt-strike-parser` Cobalt Strike log parser binary)
|
||||
|
||||
There are three options to run RedEye in Red Team mode:
|
||||
|
||||
1. Run the downloaded binary, passing in the `--redTeam` and password options:
|
||||
```
|
||||
./RedEye --redTeam --password <your_password>
|
||||
```
|
||||
2. Clone, install, and run the project directly (covered in the [Local Build](#local-build) section).
|
||||
3. Docker Compose
|
||||
1. Clone the repo
|
||||
2. Update the environment variables in `docker-compose.yml`.
|
||||
3. Run:
|
||||
```
|
||||
docker-compose -f docker-compose.yml up -d redeye-core
|
||||
```
|
||||
|
||||
### Blue Team
|
||||
|
||||
The Blue Team mode is a simplified, read-only UI for displaying data that has been curated, annotated, and exported by a Red Team. This mode runs by default to make startup more simple for the Blue Team.
|
||||
|
||||
The Blue Team version can be run by double-clicking the 'RedEye' application binary. RedEye runs at http://127.0.0.1:4000 (by default) and will automatically open your default browser.
|
||||
|
||||
### Blue Team Presentation Handoff
|
||||
|
||||
If a `campaigns` folder is located in the same directory as the `RedEye` application, RedEye will attempt to import any `.redeye` campaign files within. Campaign files can be exported in the Red Team mode.
|
||||
|
||||
To prepare a version for the Blue Team, follow these two steps:
|
||||
|
||||
1. Copy the `RedEye` application binary to an empty folder.
|
||||
2. Create a `campaigns` folder in the same directory and place the `.redeye` campaign files you want to send inside.
|
||||
|
||||
```
|
||||
Folder/
|
||||
RedEye
|
||||
campaigns/
|
||||
Campaign-01.redeye
|
||||
Campaign-02.redeye
|
||||
```
|
||||
|
||||
`.redeye` files can also be uploaded in Blue Team mode via the "+ Add Campaign" dialog.
|
||||
|
||||
## Example Datasets
|
||||
There are example datasets in this repo available for download. These are located in the [./applications/redeye-e2e/src/fixtures](https://github.com/cisagov/RedEye/tree/develop/applications/redeye-e2e/src/fixtures) folder.
|
||||
- **gtdataset** - available as [gt.redeye](https://github.com/cisagov/RedEye/raw/develop/applications/redeye-e2e/src/fixtures/gt.redeye) and as [Cobalt Strike Logs](https://github.com/cisagov/RedEye/tree/develop/applications/redeye-e2e/src/fixtures/gtdataset)
|
||||
- **smalldata** - available as [smalldata.redeye](https://github.com/cisagov/RedEye/raw/develop/applications/redeye-e2e/src/fixtures/smalldata.redeye) and as [Cobalt Strike Logs](https://github.com/cisagov/RedEye/tree/develop/applications/redeye-e2e/src/fixtures/smalldata)
|
||||
- **testdata** - available as [Cobalt Strike Logs](https://github.com/cisagov/RedEye/tree/develop/applications/redeye-e2e/src/fixtures/testdata)
|
||||
|
||||
You may want to use a tool like [download-directory.github.io](https://download-directory.github.io/) to download just one folder of a github repo
|
||||
|
||||
<!--
|
||||
## RedEye Server Settings
|
||||
RedEye runs as a server and can be setup to serve the UI on a network..
|
||||
|
||||
***{instructions}***
|
||||
-->
|
||||
|
||||
## RedEye Server Parameters
|
||||
|
||||
Type `./Redeye -h` to view the options
|
||||
|
||||
```
|
||||
-d, --developmentMode [boolean] put the database and server in development mode
|
||||
-r, --redTeam [boolean] run the server in red team mode
|
||||
--port [number] the port the server should be exposed at
|
||||
-p, --password [string] the password for user authentication
|
||||
--parsers [string...] A list of parsers to use or a flag to use all parsers in the parsers folder
|
||||
-t, --childProcesses [number] max # of child processes the parser can use
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
you can also configure the server parameters in a `config.json` file that sits next to the `RedEye` binary
|
||||
```json5
|
||||
|
||||
{
|
||||
"password": "937038570",
|
||||
"redTeam": true,
|
||||
"parsers": ["cobalt-strike-parser", "brute-ratel-parser"] // or true/false
|
||||
}
|
||||
```
|
||||
|
||||
## Local Build
|
||||
|
||||
### Required Packages
|
||||
|
||||
- [Node.js](https://nodejs.org/en/) >= v16
|
||||
- Install yarn: `npm install -g yarn`
|
||||
- Run: `yarn install` // Installs all packages
|
||||
- Run either:
|
||||
1. `yarn release:all` to build a binary for Linux, macOS, and Windows
|
||||
2. `yarn release:(mac|windows|linux)` .
|
||||
- platform options:
|
||||
- mac
|
||||
- windows
|
||||
- linux
|
||||
|
||||
## Development
|
||||
|
||||
### Setup
|
||||
|
||||
Install [Node.js](https://nodejs.org/en/) >= v16
|
||||
Install [yarn](https://yarnpkg.com/) globally via [npm](https://www.npmjs.com/package/yarn)
|
||||
|
||||
```
|
||||
npm install -g yarn
|
||||
```
|
||||
|
||||
Install package dependencies
|
||||
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
#### Quick Start Development
|
||||
|
||||
Runs the project in development mode
|
||||
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
#### Advanced Development
|
||||
|
||||
It is recommended to run the server and client in two separate terminals
|
||||
|
||||
```sh
|
||||
yarn start:client
|
||||
```
|
||||
|
||||
...in another terminal
|
||||
|
||||
```sh
|
||||
yarn start:server
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
to build a binary for Linux, macOS, and Windows
|
||||
|
||||
```shell
|
||||
yarn release:all
|
||||
```
|
||||
|
||||
to build for a specific platform, replace `all` with the platform name
|
||||
|
||||
```shell
|
||||
yarn release:(mac|windows|linux)
|
||||
```
|
||||
|
||||
## Platform support
|
||||
|
||||
- Linux
|
||||
- Ubuntu 18 and newer
|
||||
- Kali Linux 2020.1 and newer
|
||||
- Others may be supported but are untested
|
||||
- macOS
|
||||
- El Capitan and newer
|
||||
- Windows
|
||||
- Windows 7 and newer
|
||||
- ARM support is experimental
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
<img alt="CISA Logo" src="https://github.com/cisagov/RedEye/blob/develop/docs/images/CISA Logo.png?raw=true" height="35%" width="35%"/>
|
||||
|
||||
<img alt="RedEye Logo" src="https://raw.githubusercontent.com/cisagov/RedEye/2e0279ad4bdc798eb2ee6aa018bcd6ad66079d0e/applications/client/public/logos/Logo-Dark.svg" height="35%" width="35%"/>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`cisagov/RedEye`
|
||||
- 原始仓库:https://github.com/cisagov/RedEye
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,2 @@
|
||||
src/**/index.ts
|
||||
src/views/Campaigns/Upload/file-worker-js.js
|
||||
@@ -0,0 +1,80 @@
|
||||
root: true
|
||||
parser: '@typescript-eslint/parser'
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
- simple-import-sort
|
||||
- optimize-regex
|
||||
- no-use-extend-native
|
||||
- array-func
|
||||
- import
|
||||
- only-warn
|
||||
extends:
|
||||
- plugin:no-unsanitized/DOM
|
||||
- plugin:import/errors
|
||||
- plugin:import/warnings
|
||||
- plugin:array-func/all
|
||||
- plugin:no-use-extend-native/recommended
|
||||
- plugin:optimize-regex/recommended
|
||||
- airbnb
|
||||
- airbnb-typescript
|
||||
- prettier
|
||||
globals:
|
||||
globalThis: false
|
||||
rules:
|
||||
react-hooks/exhaustive-deps: 0 # Disabled for mobx
|
||||
react/no-unknown-property: 0 # Disabled for css and cy-test
|
||||
import/prefer-default-export: 0 # Prefer named exports
|
||||
import/no-default-export: 2 # Prefer named exports
|
||||
no-console: 1 # Warn about console logs, use window.console.log if the log should be available in production
|
||||
import/no-cycle: 0 # import cycling check too aggressive
|
||||
no-empty-pattern: 0
|
||||
'react/jsx-no-useless-fragment': 0
|
||||
'@typescript-eslint/object-curly-spacing': 1
|
||||
'@typescript-eslint/consistent-type-imports':
|
||||
- warn
|
||||
- prefer: type-imports
|
||||
disallowTypeAnnotations: true
|
||||
'react/function-component-definition':
|
||||
- 2
|
||||
- namedComponents: 'arrow-function'
|
||||
unnamedComponents: 'arrow-function'
|
||||
react/require-default-props: 0 # No default props
|
||||
no-unused-vars: 0 # Disabled for typescript-eslint no-unused-vars rule
|
||||
max-len: 0 # Disable max line length check, prettier takes care of this
|
||||
no-else-return: 0 # Allow return in if statements
|
||||
no-param-reassign: 0 # Allow assigning property of passed object
|
||||
class-methods-use-this: 0 # Don't require all methods to use this
|
||||
no-plusplus: 0 # Allow ++
|
||||
consistent-return: 0 # Can return multiple types from function
|
||||
no-return-assign: 0 # Allow arrow function return
|
||||
prefer-destructuring: 0 # Try not to destructure objects, especially with mobx
|
||||
no-restricted-syntax: 0 # Stops strange eslint errors
|
||||
react/state-in-constructor: 0 # For class components
|
||||
lines-between-class-members: 0 # Prevent new lines being placed between class properties
|
||||
react/jsx-props-no-spreading: 0
|
||||
func-names: 0
|
||||
no-underscore-dangle: 0
|
||||
jsx-a11y/click-events-have-key-events: 0
|
||||
no-nested-ternary: 0
|
||||
'@typescript-eslint/indent': 0
|
||||
'@typescript-eslint/semi': 0
|
||||
react/jsx-indent: 0
|
||||
react/jsx-closing-bracket-location: 0
|
||||
react/jsx-indent-props: 0
|
||||
react/jsx-one-expression-per-line: 0
|
||||
react/jsx-curly-newline: 0
|
||||
react/jsx-wrap-multilines: 0
|
||||
'@typescript-eslint/comma-dangle': 0
|
||||
import/no-extraneous-dependencies: 0
|
||||
import/extensions: 0
|
||||
'@typescript-eslint/lines-between-class-members': 0 # Prevent new lines being placed between class properties
|
||||
react/destructuring-assignment: 0 # Mobx needs non destructuring assignments
|
||||
'@typescript-eslint/no-use-before-define': 0 # Allow styled components to be placed below function
|
||||
'@typescript-eslint/no-unused-vars': 1 # Enable this to replace default eslint option
|
||||
'@typescript-eslint/ban-ts-comment': 0 # Allow ts-ignore, use wisely
|
||||
'@typescript-eslint/no-explicit-any': 0 # Allow explicit any, use wisely
|
||||
'@typescript-eslint/return-await': 0 # Allow returning await for react-query
|
||||
react/react-in-jsx-scope: 0 # React 17 doesn't require react to be imported anymore
|
||||
react/no-children-prop: 0 # children are great!
|
||||
parserOptions:
|
||||
project: './tsconfig.json'
|
||||
@@ -0,0 +1,20 @@
|
||||
# RedEye Client
|
||||
|
||||
The client is a React application using Mobx for state management and Blueprint for its component library.
|
||||
|
||||
To generate graphql models, run: `yarn moon run @redeye/client:generate-graphql`
|
||||
|
||||
## Core Libraries
|
||||
|
||||
State Management: Mobx & Mobx Keystone
|
||||
Routing: React Router
|
||||
Component Library: Blueprint
|
||||
Data Fetching: GraphQL (through mkgql) and Tanstack Query
|
||||
|
||||
## Structure
|
||||
|
||||
The client is split into three primary folders:
|
||||
|
||||
- [Store](./src/store): The global store and GraphQL models as Mobx Keystone classes
|
||||
- [Views](./src/views): Each folder represents a view (loosely corresponding to its route) with components only used in that view
|
||||
- [Components](./src/components): Reusable or similarly grouped components across views
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"delete": true,
|
||||
"location": "all",
|
||||
"verbose": false,
|
||||
"singleQuotes": true,
|
||||
"exclude": ["graphql/[A-Z0-9_\\.-]+", "file-worker"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="hsl(0,0%,9%)" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="description" content="RedEye: Red Team C2 Log Visualization" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>RedEye</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'client',
|
||||
preset: '../../jest.preset.js',
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
},
|
||||
},
|
||||
coverageDirectory: '../../coverage/applications/client',
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
dependsOn:
|
||||
- 'graph'
|
||||
- 'ui-styles'
|
||||
tasks:
|
||||
build:
|
||||
deps:
|
||||
- ~:build-vite
|
||||
start-dev:
|
||||
local: true
|
||||
deps:
|
||||
- ^:build
|
||||
- ~:start-vite
|
||||
options:
|
||||
runInCI: false
|
||||
runDepsInParallel: false
|
||||
graphql:
|
||||
command: mk-gql --outDir=./src/store/graphql ../server/schema.graphql
|
||||
options:
|
||||
runInCI: false
|
||||
lint-graphql:
|
||||
command: 'eslint src/store/graphql --fix'
|
||||
options:
|
||||
runInCI: false
|
||||
generate-graphql:
|
||||
command: 'prettier --write src/store/graphql/*.ts'
|
||||
deps:
|
||||
- ~:graphql
|
||||
- ~:lint-graphql
|
||||
options:
|
||||
runInCI: false
|
||||
runDepsInParallel: false
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@redeye/client",
|
||||
"version": "0.9.0",
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@redeye/graph": "workspace:*",
|
||||
"@redeye/ui-styles": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"
|
||||
viewBox="0 0 32 32">
|
||||
<style>
|
||||
.ribbon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.background {
|
||||
fill: black;
|
||||
}
|
||||
|
||||
.cube {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.ribbon {
|
||||
fill: black;
|
||||
}
|
||||
|
||||
.background {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<polygon class="background"
|
||||
points="25 13 24.5664 12.2567 27.8722 10.3677 23.5744 3 8.4197 3 3.8483 11 0 11 0 19 7 19 7.4336 19.7433 5.0039 21.1318 4.1278 21.6323 8.4256 29 23.5803 29 28.1517 21 32 21 32 13 25 13" />
|
||||
<g class="ribbon">
|
||||
<polygon points="1 17.125 1 12 3.875 12 1 17.125" />
|
||||
<polygon points="19.7917 10.5 21.25 13 25.625 10.5 19.7917 10.5" />
|
||||
<polygon points="26.5 10 23 4 9 4 1 18 8 18 12.5 10 26.5 10" />
|
||||
<polygon points="28.125 20 31 20 31 14.875 28.125 20" />
|
||||
<polygon points="12.2083 21.5 10.75 19 6.375 21.5 12.2083 21.5" />
|
||||
<polygon points="5.5 22 9 28 23 28 31 14 24 14 19.5 22 5.5 22" />
|
||||
</g>
|
||||
<polygon class="cube" points="17.75 13 14.25 13 12.5 16 14.25 19 17.75 19 19.5 16 17.75 13" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_13" data-name="Layer 13" xmlns="http://www.w3.org/2000/svg" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#linear-gradient-6);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#linear-gradient-3);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: url(#linear-gradient-4);
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: url(#linear-gradient-5);
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: url(#radial-gradient);
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
opacity: .15;
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
opacity: .3;
|
||||
}
|
||||
</style>
|
||||
<radialGradient id="radial-gradient" cx="134.8386" cy="116.6086" fx="134.8386" fy="116.6086" r="40.159" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="red"/>
|
||||
<stop offset=".1498" stop-color="#f90000"/>
|
||||
<stop offset=".3381" stop-color="#ea0000"/>
|
||||
<stop offset=".547" stop-color="#d00000"/>
|
||||
<stop offset=".7707" stop-color="#ac0000"/>
|
||||
<stop offset="1" stop-color="maroon"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="linear-gradient" x1="178.2987" y1="45.0278" x2="2.1541" y2="133.8138" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".3982" stop-color="#ccc"/>
|
||||
<stop offset=".4986" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#4d4d4d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-2" x1="13.2721" y1="94.4971" x2="30.793" y2="133.2128" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".001" stop-color="#fff"/>
|
||||
<stop offset="1" stop-color="#999"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-3" x1="169.9339" y1="109.8548" x2="182.0855" y2="80.1822" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".001" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#666"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-4" x1="240.239" y1="118.1809" x2="74.372" y2="216.1024" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".4695" stop-color="#ccc"/>
|
||||
<stop offset=".5872" stop-color="#999"/>
|
||||
<stop offset="1" stop-color="#4d4d4d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-5" x1="241.8381" y1="160.4242" x2="225.4475" y2="122.8389" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".012" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#666"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-6" x1="85.7089" y1="145.4674" x2="74.9702" y2="173.7271" xlink:href="#linear-gradient-2"/>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-7" d="M139.7025,104h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.031l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.031l-11.6667-20c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845Z"/>
|
||||
<g class="cls-9">
|
||||
<path class="cls-6" d="M139.7025,105c1.0629,0,2.0558,.5703,2.5913,1.4883l11.6667,20c.5439,.9324,.5439,2.0909,0,3.0233l-11.6667,20c-.5355,.918-1.5285,1.4883-2.5913,1.4883h-23.405c-1.0629,0-2.0558-.5703-2.5913-1.4883l-11.6667-20c-.5439-.9324-.5439-2.0909,0-3.0233l11.6667-20c.5355-.918,1.5285-1.4883,2.5913-1.4883h23.405m0-1h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.031l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.031l-11.6667-20c-.7169-1.2289-2.0324-1.9845-3.4551-1.9845h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g class="cls-11">
|
||||
<path class="cls-6" d="M130.2975,128h22.2204c1.5436,0,2.5053-1.6744,1.7276-3.0077l-11.0878-19.0077c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845h-22.2204c-1.5436,0-2.5053,1.6744-1.7276,3.0077l11.0878,19.0077c.7168,1.2289,2.0324,1.9845,3.4551,1.9845Z"/>
|
||||
</g>
|
||||
<g class="cls-10">
|
||||
<path d="M126.8243,130.0155l-11.0967,19.023c-.7718,1.323-2.6834,1.323-3.4551,0l-11.0967-19.023c-.7265-1.2455-.7265-2.7855,0-4.031l11.0967-19.023c.7718-1.323,2.6834-1.323,3.4551,0l11.0967,19.023c.7265,1.2455,.7265,2.7855,0,4.031Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-8" d="M208.86,84.2906l-25.551-44.2884c-1.4287-2.4765-4.0704-4.0022-6.9295-4.0022H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9768l30.3695-52.0619c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path class="cls-6" d="M176.3795,37c2.4938,0,4.8171,1.3419,6.0633,3.5019l25.551,44.2884c.4199,.7278,.5161,1.5713,.2708,2.3749-.2453,.8036-.796,1.4496-1.5507,1.819l-42.3075,20.7036-12.5549-22.1804c-.1775-.3136-.5099-.5074-.8703-.5074h-40.2375c-3.8971,0-7.5379,2.0912-9.5016,5.4575l-30.3694,52.0619c-.8926,1.5302-2.5475,2.4807-4.3189,2.4807H13.565c-1.5243,0-2.8055-1.1419-2.9802-2.6561l-5.4628-47.3439H40c.3557,0,.6846-.1889,.8638-.4962l32.1059-55.0387c1.6066-2.7542,4.5855-4.4651,7.774-4.4651h95.6358m0-1H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9767l30.3694-52.062c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5918l-25.551-44.2884c-1.4287-2.4764-4.0704-4.0022-6.9295-4.0022h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-3" d="M9.4796,143.4895l-5.4796-47.4895H40l-27.731,48.0672c-.7224,1.2521-2.6237,.8584-2.7894-.5776Z"/>
|
||||
<path class="cls-2" d="M164,111l41.1816-20.1527c1.428-.6988,.9305-2.8473-.6593-2.8473h-53.5222l13,23Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-4" d="M47.14,171.7094l25.551,44.2884c1.4287,2.4765,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9768l-30.3695,52.0619c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path class="cls-6" d="M242.435,109c1.5243,0,2.8055,1.1419,2.9802,2.6561l5.4628,47.3439h-34.878c-.3557,0-.6846,.1889-.8638,.4962l-32.1059,55.0387c-1.6066,2.7542-4.5855,4.4651-7.774,4.4651H79.6205c-2.4938,0-4.8171-1.3419-6.0633-3.5019l-25.551-44.2884c-.4199-.7278-.5161-1.5713-.2708-2.3749,.2453-.8036,.796-1.4496,1.5507-1.819l42.3075-20.7036,12.5549,22.1804c.1775,.3136,.5099,.5074,.8702,.5074h40.2375c3.8971,0,7.5379-2.0912,9.5016-5.4575l30.3694-52.0619c.8926-1.5302,2.5475-2.4807,4.3189-2.4807h52.9888m0-1h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9767l-30.3694,52.062c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918l25.551,44.2884c1.4287,2.4764,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-5" d="M246.5204,112.5105l5.4796,47.4895h-36l27.731-48.0672c.7224-1.2521,2.6237-.8584,2.7894,.5776Z"/>
|
||||
<path class="cls-1" d="M92,145l-41.1816,20.1527c-1.428,.6988-.9305,2.8473,.6593,2.8473h53.5222l-13-23Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_13" data-name="Layer 13" xmlns="http://www.w3.org/2000/svg" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#linear-gradient-6);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#linear-gradient-3);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: url(#linear-gradient-4);
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: url(#linear-gradient-5);
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: url(#radial-gradient);
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
opacity: .15;
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
opacity: .3;
|
||||
}
|
||||
</style>
|
||||
<radialGradient id="radial-gradient" cx="134.8386" cy="116.6086" fx="134.8386" fy="116.6086" r="40.159" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="red"/>
|
||||
<stop offset=".1498" stop-color="#f90000"/>
|
||||
<stop offset=".3381" stop-color="#ea0000"/>
|
||||
<stop offset=".547" stop-color="#d00000"/>
|
||||
<stop offset=".7707" stop-color="#ac0000"/>
|
||||
<stop offset="1" stop-color="maroon"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="linear-gradient" x1="181.204" y1="38.5062" x2="12.8241" y2="144.8752" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b3b3b3"/>
|
||||
<stop offset=".4112" stop-color="#4d4d4d"/>
|
||||
<stop offset=".5089" stop-color="#333"/>
|
||||
<stop offset="1" stop-color="#000"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-2" x1="17.6659" y1="93.9885" x2="29.999" y2="138.9568" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#333"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-3" x1="174.0229" y1="113.5193" x2="179.62" y2="82.7355" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".001" stop-color="gray"/>
|
||||
<stop offset="1" stop-color="#1a1a1a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-4" x1="247.5646" y1="110.128" x2="74.5378" y2="217.1234" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b3b3b3"/>
|
||||
<stop offset=".5042" stop-color="#4d4d4d"/>
|
||||
<stop offset=".6029" stop-color="#333"/>
|
||||
<stop offset="1" stop-color="#000"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-5" x1="249.131" y1="166.3848" x2="221.4395" y2="119.4912" xlink:href="#linear-gradient-3"/>
|
||||
<linearGradient id="linear-gradient-6" x1="92.258" y1="147.6046" x2="67.9713" y2="179.4808" xlink:href="#linear-gradient-2"/>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-7" d="M139.7025,104h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.031l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.031l-11.6667-20c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845Z"/>
|
||||
<g class="cls-9">
|
||||
<path d="M139.7025,105c1.0629,0,2.0558,.5703,2.5913,1.4884l11.6667,20c.5439,.9324,.5439,2.0908,0,3.0232l-11.6667,20c-.5356,.9181-1.5285,1.4884-2.5913,1.4884h-23.405c-1.0628,0-2.0558-.5703-2.5913-1.4884l-11.6667-20c-.5439-.9324-.5439-2.0908,0-3.0232l11.6667-20c.5355-.9181,1.5285-1.4884,2.5913-1.4884h23.405m0-1h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.0309l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.0309l-11.6667-20c-.7169-1.2289-2.0324-1.9845-3.4551-1.9845h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g class="cls-11">
|
||||
<path class="cls-6" d="M130.2975,128h22.2204c1.5436,0,2.5053-1.6744,1.7276-3.0077l-11.0878-19.0077c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845h-22.2204c-1.5436,0-2.5053,1.6744-1.7276,3.0077l11.0878,19.0077c.7168,1.2289,2.0324,1.9845,3.4551,1.9845Z"/>
|
||||
</g>
|
||||
<g class="cls-10">
|
||||
<path d="M126.8243,130.0155l-11.0967,19.023c-.7718,1.323-2.6834,1.323-3.4551,0l-11.0967-19.023c-.7265-1.2455-.7265-2.7855,0-4.031l11.0967-19.023c.7718-1.323,2.6834-1.323,3.4551,0l11.0967,19.023c.7265,1.2455,.7265,2.7855,0,4.031Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-8" d="M208.86,84.2906l-25.551-44.2884c-1.4287-2.4765-4.0704-4.0022-6.9295-4.0022H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9768l30.3695-52.0619c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path d="M176.3795,37c2.4938,0,4.8171,1.3419,6.0633,3.502l25.551,44.2884c.4199,.7278,.5161,1.5712,.2708,2.3748-.2452,.8036-.796,1.4496-1.5507,1.8189l-42.3075,20.7037-12.5549-22.1804-.2872-.5074h-40.8206c-3.8971,0-7.5379,2.0912-9.5016,5.4575l-30.3694,52.0619c-.8926,1.5301-2.5475,2.4807-4.3189,2.4807H13.565c-1.5243,0-2.8055-1.1419-2.9802-2.6561l-5.4628-47.3439H40.5744l.2894-.4961,32.1059-55.0387c1.6066-2.7542,4.5855-4.4652,7.774-4.4652h95.6358m0-1H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9768l30.3694-52.0619c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5917l-25.551-44.2884c-1.4287-2.4765-4.0704-4.0022-6.9295-4.0022h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-3" d="M9.4796,143.4895l-5.4796-47.4895H40l-27.731,48.0672c-.7224,1.2521-2.6237,.8584-2.7894-.5776Z"/>
|
||||
<path class="cls-2" d="M164,111l41.1816-20.1527c1.428-.6988,.9305-2.8473-.6593-2.8473h-53.5222l13,23Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-4" d="M47.14,171.7094l25.551,44.2884c1.4287,2.4765,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9768l-30.3695,52.0619c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path d="M242.435,109c1.5243,0,2.8055,1.1419,2.9802,2.6561l5.4628,47.3439h-35.4523l-.2894,.4962-32.1059,55.0387c-1.6066,2.7542-4.5855,4.4651-7.774,4.4651H79.6205c-2.4938,0-4.8171-1.3419-6.0633-3.5019l-25.551-44.2884c-.4199-.7278-.5161-1.5713-.2708-2.3749,.2453-.8036,.796-1.4496,1.5507-1.819l42.3075-20.7036,12.5549,22.1804,.2872,.5074h40.8206c3.8971,0,7.5379-2.0912,9.5016-5.4575l30.3694-52.0619c.8926-1.5301,2.5475-2.4807,4.3189-2.4807h52.9888m0-1h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9768l-30.3694,52.0619c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918l25.551,44.2884c1.4287,2.4764,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-5" d="M246.5204,112.5105l5.4796,47.4895h-36l27.731-48.0672c.7224-1.2521,2.6237-.8584,2.7894,.5776Z"/>
|
||||
<path class="cls-1" d="M92,145l-41.1816,20.1527c-1.428,.6988-.9305,2.8473,.6593,2.8473h53.5222l-13-23Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"short_name": "RedEye",
|
||||
"name": "Red Team C2 Log Visualization",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"sizes": "32x32",
|
||||
"type": "image/svg"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "32x32 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logos/Logo-Dark.png",
|
||||
"type": "image/svg",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FocusStyleManager, Spinner } from '@blueprintjs/core';
|
||||
import { css } from '@emotion/react';
|
||||
import { RedEyeRoutes } from '@redeye/client/store';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
FocusStyleManager.onlyShowFocusOnTabs();
|
||||
const Login = lazy(() => import('./views/Login'));
|
||||
const Campaign = lazy(() => import('./views/Campaign/Campaign'));
|
||||
const CampaignList = lazy(() => import('./views/Campaigns/Campaigns'));
|
||||
|
||||
export const App = observer(() => (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Spinner
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route path={RedEyeRoutes.LOGIN} element={<Login />} />
|
||||
<Route path={RedEyeRoutes.CAMPAIGNS_LIST} element={<CampaignList />} />
|
||||
<Route path={`${RedEyeRoutes.CAMPAIGN}/*`} element={<Campaign />} />
|
||||
<Route path="*" element={<Navigate to={RedEyeRoutes.LOGIN} />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
));
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { History, Update } from 'history';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createState } from './components/mobx-create-state';
|
||||
|
||||
export interface BrowserRouterProps {
|
||||
basename?: string;
|
||||
children?: ReactNode;
|
||||
history: History;
|
||||
}
|
||||
|
||||
export const CustomRouter = observer<BrowserRouterProps>(({ basename, children, history }) => {
|
||||
const state = createState({
|
||||
action: history.action,
|
||||
location: history.location,
|
||||
setState(newState: Update) {
|
||||
this.action = newState.action;
|
||||
this.location = newState.location;
|
||||
},
|
||||
});
|
||||
|
||||
useLayoutEffect(() => history.listen(state.setState), [history]);
|
||||
|
||||
return (
|
||||
<Router
|
||||
basename={basename}
|
||||
children={children}
|
||||
location={state.location}
|
||||
navigationType={state.action}
|
||||
navigator={history}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import type { DialogExProps } from '@redeye/client/components';
|
||||
import { DialogBodyEx, DialogEx, DialogFooterEx } from '@redeye/client/components';
|
||||
import { RedEyeRoutes, useStore } from '@redeye/client/store';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type AuthCheckProps = Partial<DialogExProps> & {};
|
||||
|
||||
export const AuthCheck = observer<AuthCheckProps>(({ ...props }) => {
|
||||
const store = useStore();
|
||||
|
||||
// Redirect to login if the app loads with no session/connection to the server
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (!store.appMeta.blueTeam) store.auth.checkServerConnection();
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const onClose = () => {
|
||||
store.auth.setPromptAuth(false);
|
||||
store.auth.setHasClickedAuthDialog(true);
|
||||
};
|
||||
|
||||
const onLogin = () => {
|
||||
onClose();
|
||||
store.router.updateRoute({ path: RedEyeRoutes.LOGIN });
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogEx isOpen={store.auth.promptAuth} title="Unable to authenticate" onClose={onClose} {...props}>
|
||||
<DialogBodyEx>
|
||||
The application was unable to authenticate with the server. Click "Login" to re-authenticate.
|
||||
</DialogBodyEx>
|
||||
<DialogFooterEx
|
||||
actions={
|
||||
<>
|
||||
<Button onClick={onClose} text="Close" />
|
||||
<Button intent={Intent.PRIMARY} onClick={onLogin} text="Login" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DialogEx>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { AddComment16, Chat16, List16, Tag16, User16 } from '@carbon/icons-react';
|
||||
import type { CarbonIconType } from '@carbon/icons-react';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
|
||||
export type CarbonIconProps = ComponentProps<'span'> & {
|
||||
icon: CarbonIconType | CustomIcon;
|
||||
size?: number;
|
||||
// SEARCH FOR ICONS HERE: https://www.carbondesignsystem.com/guidelines/icons/library/
|
||||
};
|
||||
|
||||
/** Maps CarbonIcons to Blueprint <Icon/> */
|
||||
export const CarbonIcon: FC<CarbonIconProps> = ({ icon, size = 16, className, ...props }) => {
|
||||
const CarbonIconComponent =
|
||||
typeof icon === 'string'
|
||||
? () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
// xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
>
|
||||
<path d={icon} />
|
||||
</svg>
|
||||
)
|
||||
: icon; // CarbonIconType
|
||||
|
||||
return (
|
||||
<span {...props} className={[Classes.ICON, className].join(' ')}>
|
||||
<CarbonIconComponent />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/** the d="" attribute for an svg <path d={string} /> */
|
||||
export type SvgPathD = string; // ComponentProps<'path'>['d'];
|
||||
export type CustomIcon = string; // keyof typeof customIconPaths;
|
||||
|
||||
/** custom icon paths for use in <CarbonIcon icon={customIconPaths.icon} /> */
|
||||
export const customIconPaths = {
|
||||
multiComment16:
|
||||
'M9,15.5l2-3.5h3c0.6,0,1-0.4,1-1V5c0-0.6-0.4-1-1-1h-2V3h2c1.1,0,2,0.9,2,2v6c0,1.1-0.9,2-2,2c0,0,0,0,0,0h-2.4l-1.7,3 L9,15.5z M4,13c-1.1,0-2-0.9-2-2v-1h1v1c0,0.6,0.4,1,1,1h4.5v1H4z M3,9V7h8v2H3z M0,9V7h2v2H0z M3,6V4h8v2H3z M0,6V4h2v2H0z M3,3V1 h8v2H3z M0,3V1h2v2H0z',
|
||||
beacon16:
|
||||
'M13.1464 13.8536L9.73826 10.4454C10.0115 10.2508 10.2508 10.0115 10.4454 9.73827L13.8535 13.1464L13.1464 13.8536ZM5.55462 6.26174C5.74922 5.98848 5.98847 5.74922 6.26173 5.55463L2.85355 2.14645L2.14644 2.85355L5.55462 6.26174ZM10 8C10 9.10457 9.10457 10 8 10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6C9.10457 6 10 6.89543 10 8Z',
|
||||
host16:
|
||||
'M2 8C2 6.4087 2.63259 4.88254 3.75781 3.75732C4.88303 2.63211 6.4087 2 8 2C9.5913 2 11.117 2.63211 12.2422 3.75732C13.3674 4.88254 14 6.4087 14 8C14 9.5913 13.3674 11.1175 12.2422 12.2427C11.117 13.3679 9.5913 14 8 14C6.4087 14 4.88303 13.3679 3.75781 12.2427C2.63259 11.1175 2 9.5913 2 8ZM3 8C3.00159 9.32559 3.52946 10.5965 4.4668 11.5338C5.40413 12.4712 6.67441 12.9984 8 13C9.32608 13 10.5975 12.4732 11.5352 11.5355C12.4728 10.5978 13 9.32608 13 8C13 6.67392 12.4728 5.40216 11.5352 4.46448C10.5975 3.5268 9.32608 3 8 3C6.67441 3.00159 5.40413 3.52885 4.4668 4.46619C3.52946 5.40352 3.00159 6.67441 3 8ZM7 10.5C7 10.1022 7.15815 9.72064 7.43945 9.43933C7.72076 9.15803 8.10218 9 8.5 9C8.89782 9 9.27924 9.15803 9.56055 9.43933C9.84185 9.72064 10 10.1022 10 10.5C10 10.8978 9.84185 11.2794 9.56055 11.5607C9.27924 11.842 8.89782 12 8.5 12C8.10218 12 7.72076 11.842 7.43945 11.5607C7.15815 11.2794 7 10.8978 7 10.5ZM4 8.5C4 8.10218 4.15815 7.72064 4.43945 7.43933C4.72076 7.15803 5.10218 7 5.5 7C5.89782 7 6.27924 7.15803 6.56055 7.43933C6.84185 7.72064 7 8.10218 7 8.5C7 8.89782 6.84185 9.27936 6.56055 9.56067C6.27924 9.84197 5.89782 10 5.5 10C5.10218 10 4.72076 9.84197 4.43945 9.56067C4.15815 9.27936 4 8.89782 4 8.5ZM9 7.5C9 7.10218 9.15815 6.72064 9.43945 6.43933C9.72076 6.15803 10.1022 6 10.5 6C10.8978 6 11.2792 6.15803 11.5605 6.43933C11.8419 6.72064 12 7.10218 12 7.5C12 7.89782 11.8419 8.27936 11.5605 8.56067C11.2792 8.84197 10.8978 9 10.5 9C10.1022 9 9.72076 8.84197 9.43945 8.56067C9.15815 8.27936 9 7.89782 9 7.5ZM6 5.5C6 5.10218 6.15815 4.72064 6.43945 4.43933C6.72076 4.15803 7.10218 4 7.5 4C7.89782 4 8.27924 4.15803 8.56055 4.43933C8.84185 4.72064 9 5.10218 9 5.5C9 5.89782 8.84185 6.27936 8.56055 6.56067C8.27924 6.84197 7.89782 7 7.5 7C7.10218 7 6.72076 6.84197 6.43945 6.56067C6.15815 6.27936 6 5.89782 6 5.5Z',
|
||||
link16:
|
||||
'M4 6C5.10457 6 6 5.10457 6 4C6 2.89543 5.10457 2 4 2C2.89543 2 2 2.89543 2 4C2 5.10457 2.89543 6 4 6ZM12 14C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14ZM6.44537 5.73827C6.25078 6.01153 6.01153 6.25078 5.73827 6.44537L9.55462 10.2617C9.74922 9.98847 9.98847 9.74922 10.2617 9.55463L6.44537 5.73827Z',
|
||||
linkFrom16:
|
||||
'M4 6C5.10457 6 6 5.10457 6 4C6 2.89543 5.10457 2 4 2C2.89543 2 2 2.89543 2 4C2 5.10457 2.89543 6 4 6ZM11.2617 10.5546L8.70711 8H11V7H7V11H8V8.70711L10.5546 11.2617C10.7492 10.9885 10.9885 10.7492 11.2617 10.5546ZM14 13C14 13.5523 13.5523 14 13 14C12.4477 14 12 13.5523 12 13C12 12.4477 12.4477 12 13 12C13.5523 12 14 12.4477 14 13ZM13 15C14.1046 15 15 14.1046 15 13C15 11.8954 14.1046 11 13 11C11.8954 11 11 11.8954 11 13C11 14.1046 11.8954 15 13 15Z',
|
||||
linkTo16:
|
||||
'M4 6C5.10457 6 6 5.10457 6 4C6 2.89543 5.10457 2 4 2C2.89543 2 2 2.89543 2 4C2 5.10457 2.89543 6 4 6ZM13 14C13.5523 14 14 13.5523 14 13C14 12.4477 13.5523 12 13 12C12.4477 12 12 12.4477 12 13C12 13.5523 12.4477 14 13 14ZM15 13C15 14.1046 14.1046 15 13 15C11.8954 15 11 14.1046 11 13C11 11.8954 11.8954 11 13 11C14.1046 11 15 11.8954 15 13ZM8.2929 9L5.73827 6.44537C6.01153 6.25078 6.25078 6.01152 6.44538 5.73826L9.00001 8.29289V6H10V10H6.00001V9H8.2929Z',
|
||||
teamServer16:
|
||||
'M12 6.132C12 5.95918 11.9553 5.7893 11.8701 5.63893C11.7849 5.48856 11.6622 5.36283 11.514 5.274L8.514 3.474C8.35858 3.38075 8.18075 3.33149 7.9995 3.33149C7.81825 3.33149 7.64042 3.38075 7.485 3.474L4.485 5.274C4.33694 5.36295 4.21445 5.48874 4.12946 5.6391C4.04446 5.78946 3.99986 5.95928 4 6.132V9.868C3.99996 10.0408 4.04471 10.2107 4.12989 10.3611C4.21507 10.5114 4.33776 10.6372 4.486 10.726L7.486 12.526C7.64142 12.6193 7.81925 12.6685 8.0005 12.6685C8.18175 12.6685 8.35958 12.6193 8.515 12.526L11.515 10.726C11.6631 10.637 11.7855 10.5113 11.8705 10.3609C11.9555 10.2105 12.0001 10.0407 12 9.868V6.132ZM13 6.132V9.868C13 10.2134 12.9106 10.5529 12.7404 10.8534C12.5702 11.1539 12.3252 11.4053 12.029 11.583L9.029 13.383C8.71817 13.5695 8.36249 13.668 8 13.668C7.63751 13.668 7.28183 13.5695 6.971 13.383L3.971 11.583C3.67485 11.4053 3.42976 11.1539 3.25959 10.8534C3.08943 10.5529 3 10.2134 3 9.868V6.132C3 5.78663 3.08943 5.44714 3.25959 5.1466C3.42976 4.84605 3.67485 4.5947 3.971 4.417L6.971 2.617C7.28183 2.4305 7.63751 2.33198 8 2.33198C8.36249 2.33198 8.71817 2.4305 9.029 2.617L12.029 4.417C12.3252 4.5947 12.5702 4.84605 12.7404 5.1466C12.9106 5.44714 13 5.78663 13 6.132V6.132Z',
|
||||
};
|
||||
|
||||
/** Mapping for icons that are used in several locations */
|
||||
export const semanticIcons = {
|
||||
commands: List16,
|
||||
comment: Chat16,
|
||||
addComment: AddComment16,
|
||||
operator: User16,
|
||||
beacon: customIconPaths.beacon16,
|
||||
host: customIconPaths.host16,
|
||||
link: customIconPaths.link16,
|
||||
linkTo: customIconPaths.linkTo16,
|
||||
linkFrom: customIconPaths.linkFrom16,
|
||||
teamServer: customIconPaths.teamServer16,
|
||||
tags: Tag16,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Alert, Classes } from '@blueprintjs/core';
|
||||
import styled from '@emotion/styled';
|
||||
import { AdvancedTokens } from '@redeye/ui-styles';
|
||||
|
||||
export const AlertEx = styled(Alert)`
|
||||
padding: 0;
|
||||
margin: 3rem;
|
||||
align-self: start;
|
||||
|
||||
.${Classes.ALERT_BODY} {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.${Classes.ALERT_FOOTER} {
|
||||
margin: 0;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.${Classes.BUTTON} {
|
||||
margin: 0;
|
||||
|
||||
// fill
|
||||
flex: 1 1;
|
||||
|
||||
// large
|
||||
height: ${AdvancedTokens.PtButtonHeightLarge};
|
||||
padding: 4px 16px;
|
||||
|
||||
// alignText='left'
|
||||
text-align: left;
|
||||
.${Classes.BUTTON_TEXT} {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DialogBody } from '@blueprintjs/core';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const DialogBodyEx = styled(DialogBody)`
|
||||
// to control style if we need it later
|
||||
/* padding: 1.5rem; */
|
||||
`;
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { DialogProps } from '@blueprintjs/core';
|
||||
import { Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import { Close16 } from '@carbon/icons-react';
|
||||
import { css } from '@emotion/react';
|
||||
import type { WithConditionalCSSProp } from '@emotion/react/types/jsx-namespace';
|
||||
import { CoreTokens, Header } from '@redeye/ui-styles';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import { CarbonIcon } from '../CarbonIcon';
|
||||
|
||||
export type DialogExProps = Omit<DialogProps, 'icon'> & {
|
||||
headerProps?: WithConditionalCSSProp<ComponentProps<'div'>>;
|
||||
/** The Dialog will not be larger than the screen height */
|
||||
fixedHeight?: boolean;
|
||||
/** The Dialog will be wider */
|
||||
wide?: boolean;
|
||||
};
|
||||
|
||||
export const DialogEx: FC<DialogExProps> = ({
|
||||
children,
|
||||
title,
|
||||
isCloseButtonShown = true,
|
||||
headerProps,
|
||||
fixedHeight = false,
|
||||
wide = false,
|
||||
// iconName,
|
||||
// icon,
|
||||
...props
|
||||
}) => (
|
||||
<Dialog css={[dialogStyles, wide && wideStyles, fixedHeight && fixedHeightStyles]} {...props}>
|
||||
<div
|
||||
cy-test="modal-header"
|
||||
{...headerProps}
|
||||
css={[dialogHeaderStyles, title == null && dialogHeaderEmptyStyles, headerProps?.css]}
|
||||
className={Classes.DIALOG_HEADER}
|
||||
>
|
||||
{isCloseButtonShown && (
|
||||
<Button
|
||||
cy-test="close-log"
|
||||
minimal
|
||||
onClick={props.onClose}
|
||||
css={closeButtonStyles}
|
||||
rightIcon={<CarbonIcon icon={Close16} />}
|
||||
/>
|
||||
)}
|
||||
{typeof title === 'string' ? <Header small>{title}</Header> : title}
|
||||
</div>
|
||||
{children}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const closeButtonStyles = css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0.25rem;
|
||||
color: ${CoreTokens.TextMuted};
|
||||
`;
|
||||
|
||||
const dialogStyles = css`
|
||||
// some styles applied from globalStyle in common.styles.tsx
|
||||
margin: 3rem;
|
||||
align-self: start;
|
||||
`;
|
||||
|
||||
const wideStyles = css`
|
||||
width: 100%;
|
||||
max-width: 44rem;
|
||||
`;
|
||||
|
||||
const fixedHeightStyles = css`
|
||||
min-height: 400px;
|
||||
align-self: stretch;
|
||||
`;
|
||||
|
||||
const dialogHeaderStyles = css`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
opacity: 0.97; // to see wats scrollin underneath
|
||||
background-color: ${CoreTokens.Background1};
|
||||
border-bottom: 1px solid ${CoreTokens.BorderNormal};
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
const dialogHeaderEmptyStyles = css`
|
||||
min-height: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
`;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Classes, DialogFooter } from '@blueprintjs/core';
|
||||
import styled from '@emotion/styled';
|
||||
import { AdvancedTokens } from '@redeye/ui-styles';
|
||||
|
||||
export const DialogFooterEx = styled(DialogFooter)`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0px; // 1px;
|
||||
border: none;
|
||||
|
||||
.${Classes.DIALOG_FOOTER_MAIN_SECTION} {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.${Classes.DIALOG_FOOTER_ACTIONS} {
|
||||
flex: 1 0 auto;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.${Classes.BUTTON} {
|
||||
margin: 0;
|
||||
|
||||
// fill
|
||||
flex: 1 1;
|
||||
|
||||
// large
|
||||
height: ${AdvancedTokens.PtButtonHeightLarge};
|
||||
padding: 4px 16px;
|
||||
|
||||
// alignText='left'
|
||||
text-align: left;
|
||||
.${Classes.BUTTON_TEXT} {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { DialogExProps } from '@redeye/client/components';
|
||||
import { DialogEx, SettingsForm } from '@redeye/client/components';
|
||||
import type { FC } from 'react';
|
||||
|
||||
type GeneralSettingsOverlayProps = DialogExProps & {};
|
||||
|
||||
export const GeneralSettingsOverlay: FC<GeneralSettingsOverlayProps> = ({ ...props }) => (
|
||||
<DialogEx title="General Settings" {...props}>
|
||||
<SettingsForm css={{ padding: '1rem' }} />
|
||||
</DialogEx>
|
||||
);
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { DrawerProps } from '@blueprintjs/core';
|
||||
import { css } from '@emotion/react';
|
||||
import { ExternalLink, Header, Spacer, Txt } from '@redeye/ui-styles';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { Logo } from '../Header';
|
||||
import { DialogEx } from './DialogEx';
|
||||
|
||||
const appVersion = PACKAGE_VERSION;
|
||||
|
||||
type HelpOverlayProps = DrawerProps & {};
|
||||
|
||||
export const HelpOverlay: FC<HelpOverlayProps> = ({ ...props }) => (
|
||||
<DialogEx css={layoutStyle} {...props}>
|
||||
<section cy-test="about-modal" css={{ padding: '1.5rem 3rem 2rem' }}>
|
||||
<Header cy-test="about-modal-header" large css={{ display: 'flex', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<Logo cy-test="redeye-logo" css={{ height: '4rem', marginLeft: -16 }} />
|
||||
<Spacer />
|
||||
<Txt>
|
||||
RedEye{' '}
|
||||
<Txt monospace muted normal large>
|
||||
v{appVersion}
|
||||
</Txt>
|
||||
</Txt>
|
||||
</Header>
|
||||
|
||||
<Txt cy-test="about-modal-description" tagName="p">
|
||||
RedEye is a red team C2 log visualization tool developed by <ExternalLink regular {...pnnlLink} /> for the{' '}
|
||||
<ExternalLink regular {...cisaLink} />.
|
||||
</Txt>
|
||||
|
||||
<Txt cy-test="about-modal-links" tagName="p">
|
||||
<ExternalLink {...githubLink} />
|
||||
<Spacer>|</Spacer>
|
||||
<ExternalLink {...cobaltStrikeLink} />
|
||||
</Txt>
|
||||
|
||||
<Txt cy-test="about-modal-copyright" tagName="p" muted small>
|
||||
Copyright {new Date().getFullYear()} PNNL & CISA - <ExternalLink muted {...licenseLink} />
|
||||
</Txt>
|
||||
</section>
|
||||
</DialogEx>
|
||||
);
|
||||
|
||||
const layoutStyle = css`
|
||||
width: 650px;
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type LinkInfo = {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
const githubLink: LinkInfo = {
|
||||
href: 'https://github.com/cisagov/redeye',
|
||||
children: 'Redeye GitHub Repo',
|
||||
};
|
||||
const licenseLink: LinkInfo = {
|
||||
href: `${githubLink.href}/blob/develop/LICENSE`,
|
||||
children: 'BSD-3 Licensed',
|
||||
};
|
||||
const cobaltStrikeLink: LinkInfo = {
|
||||
href: 'https://www.cobaltstrike.com',
|
||||
children: 'Cobalt Strike',
|
||||
};
|
||||
const pnnlLink: LinkInfo = {
|
||||
href: 'https://www.pnnl.gov/',
|
||||
children: 'Pacific Northwest National Laboratory',
|
||||
};
|
||||
const cisaLink: LinkInfo = {
|
||||
href: 'https://www.cisa.gov',
|
||||
children: 'CyberSecurity and Infrastructure Security Agency',
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import type { DialogExProps } from '@redeye/client/components';
|
||||
import { DialogBodyEx, DialogEx, DialogFooterEx } from '@redeye/client/components';
|
||||
import { useStore } from '@redeye/client/store';
|
||||
import { Txt } from '@redeye/ui-styles';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { FormEventHandler } from 'react';
|
||||
|
||||
type UserSettingsOverlayProps = DialogExProps & {
|
||||
onSubmit?: FormEventHandler<HTMLFormElement>;
|
||||
};
|
||||
|
||||
export const UserSettingsOverlay = observer<UserSettingsOverlayProps>(({ onSubmit, onClose, ...props }) => {
|
||||
const store = useStore();
|
||||
return (
|
||||
<DialogEx title="User" onClose={onClose} {...props}>
|
||||
<DialogBodyEx>
|
||||
<Txt>Logged in as {store.auth.userName}</Txt>
|
||||
</DialogBodyEx>
|
||||
<DialogFooterEx
|
||||
actions={
|
||||
<>
|
||||
<Button text="Close" onClick={onClose} />
|
||||
<Button
|
||||
intent="warning"
|
||||
text="Log out"
|
||||
cy-test="logout"
|
||||
onClick={() => store.auth.logOut()}
|
||||
css={{ marginLeft: -7 }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DialogEx>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from './AlertEx';
|
||||
export * from './DialogFooterEx';
|
||||
export * from './DialogEx';
|
||||
export * from './DialogBodyEx';
|
||||
export * from './GeneralSettingsOverlay';
|
||||
export * from './HelpOverlay';
|
||||
export * from './UserSettingsOverlay';
|
||||
@@ -0,0 +1,290 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { CoreTokens } from '@redeye/ui-styles';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useGesture } from 'react-use-gesture';
|
||||
import { createState } from './mobx-create-state';
|
||||
|
||||
export type DragResizeEvent = {
|
||||
collapsedFixed: boolean;
|
||||
collapsedFluid: boolean;
|
||||
columnWidth: number;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export type CommandProps = {
|
||||
collapseFixed: () => any;
|
||||
collapseFluid: () => any;
|
||||
reset: () => any;
|
||||
};
|
||||
export type DragResizeContentRenderer = (commandProps: CommandProps) => JSX.Element;
|
||||
|
||||
export type DragResizeProps = ComponentProps<'div'> & {
|
||||
draggerRenderer?: FC<DraggerRendererProps>;
|
||||
fixedContent: DragResizeContentRenderer;
|
||||
fluidContent: DragResizeContentRenderer;
|
||||
fixedCollapsedContent?: DragResizeContentRenderer;
|
||||
fluidCollapsedContent?: DragResizeContentRenderer;
|
||||
collapsedFixed?: boolean;
|
||||
collapsedFluid?: boolean;
|
||||
columnWidth?: number; // px
|
||||
collapsedMinWidth?: number; // px
|
||||
/** these resize events are untested */
|
||||
onDragResizeStart?: (event: DragResizeEvent) => any;
|
||||
/** these resize events are untested */
|
||||
onDragResize?: (event: DragResizeEvent) => any;
|
||||
/** these resize events are untested */
|
||||
onDragResizeEnd?: (event: DragResizeEvent) => any;
|
||||
/** these resize events are untested */
|
||||
onResizeEnd?: (event: DragResizeEvent) => any;
|
||||
};
|
||||
|
||||
export const DragResize = observer<DragResizeProps>(
|
||||
({
|
||||
draggerRenderer: DraggerComponent = DefaultDraggerComponent,
|
||||
fixedContent,
|
||||
fluidContent,
|
||||
fixedCollapsedContent = () => <DefaultFixedCollapsedComponent />,
|
||||
fluidCollapsedContent = () => <DefaultFluidCollapsedComponent />,
|
||||
collapsedFixed: collapsedFixedDefault = false,
|
||||
collapsedFixed: collapsedFluidDefault = false,
|
||||
columnWidth: columnWidthDefault = 600,
|
||||
collapsedMinWidth = 300,
|
||||
...props
|
||||
}) => {
|
||||
const state = createState({
|
||||
columnWidthPrevious: getColumnWidthStorage(columnWidthDefault),
|
||||
columnWidth: getColumnWidthStorage(columnWidthDefault),
|
||||
collapsedFixed: collapsedFixedDefault && !collapsedFluidDefault,
|
||||
collapsedFluid: collapsedFluidDefault && !collapsedFixedDefault,
|
||||
isDragging: false,
|
||||
fullWidth: collapsedMinWidth * 2,
|
||||
collapsedMaxWidth: collapsedMinWidth * 2,
|
||||
get commandProps() {
|
||||
return { collapseFixed: this.collapseFixed, collapseFluid: this.collapseFluid, reset: this.reset };
|
||||
},
|
||||
// persisting the collapsedFluid state in window.localStorage breaks graph layout
|
||||
// revert 9e3203e3 to restore this functionality
|
||||
get columnWidthStorage() {
|
||||
return getColumnWidthStorage(columnWidthDefault);
|
||||
},
|
||||
set columnWidthStorage(columnWidth: number) {
|
||||
window.localStorage.setItem(columnWidthId, columnWidth.toString());
|
||||
},
|
||||
collapseFixed() {
|
||||
this.columnWidth = 0;
|
||||
if (this.collapsedFluid) this.collapsedFluid = false;
|
||||
if (!this.collapsedFixed) this.collapsedFixed = true;
|
||||
if (!this.isDragging) {
|
||||
this.columnWidthPrevious = 0;
|
||||
}
|
||||
},
|
||||
collapseFluid() {
|
||||
this.columnWidth = this.fullWidth;
|
||||
if (this.collapsedFixed) this.collapsedFixed = false;
|
||||
if (!this.collapsedFluid) this.collapsedFluid = true;
|
||||
if (!this.isDragging) {
|
||||
this.columnWidthPrevious = this.fullWidth;
|
||||
}
|
||||
},
|
||||
reset(forceWidth?: number) {
|
||||
if (this.isDragging) return;
|
||||
const setWidth = forceWidth || this.columnWidthStorage;
|
||||
this.columnWidth = setWidth;
|
||||
this.columnWidthPrevious = setWidth;
|
||||
this.columnWidthStorage = setWidth;
|
||||
if (this.collapsedFixed) this.collapsedFixed = false;
|
||||
if (this.collapsedFluid) this.collapsedFluid = false;
|
||||
},
|
||||
calculateCollapsedMaxWidth() {
|
||||
if (wrapperElementRef?.current == null) return;
|
||||
this.fullWidth = wrapperElementRef.current?.clientWidth;
|
||||
this.collapsedMaxWidth = (wrapperElementRef?.current?.clientWidth || 0) - collapsedMinWidth;
|
||||
},
|
||||
setColumnWidthWithinBounds(calculatedColumnWidth: number) {
|
||||
// should we clamp the fixedColumn
|
||||
if (calculatedColumnWidth < collapsedMinWidth) {
|
||||
if (calculatedColumnWidth < collapsedMinWidth / 2) {
|
||||
this.collapseFixed();
|
||||
} else {
|
||||
this.columnWidth = collapsedMinWidth;
|
||||
if (this.collapsedFixed) this.collapsedFixed = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// should we clamp the fluidColumn
|
||||
if (calculatedColumnWidth > this.collapsedMaxWidth) {
|
||||
if (calculatedColumnWidth > this.collapsedMaxWidth + collapsedMinWidth / 2) {
|
||||
this.collapseFluid();
|
||||
} else {
|
||||
this.columnWidth = this.collapsedMaxWidth;
|
||||
if (this.collapsedFluid) this.collapsedFluid = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// normal resize
|
||||
this.columnWidth = calculatedColumnWidth;
|
||||
if (this.collapsedFixed) this.collapsedFixed = false;
|
||||
if (this.collapsedFluid) this.collapsedFluid = false;
|
||||
},
|
||||
});
|
||||
|
||||
const wrapperElementRef = useRef<HTMLDivElement>(null);
|
||||
const dragElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// update the full and max width when the screen changes size
|
||||
useEffect(() => {
|
||||
state.calculateCollapsedMaxWidth();
|
||||
window.addEventListener('resize', state.calculateCollapsedMaxWidth);
|
||||
return () => window.removeEventListener('resize', state.calculateCollapsedMaxWidth);
|
||||
}, [wrapperElementRef?.current]);
|
||||
|
||||
const bind = useGesture({
|
||||
onDrag: ({ movement }) => {
|
||||
const calculatedColumnWidth = state.columnWidthPrevious + Math.round(movement[0]);
|
||||
state.setColumnWidthWithinBounds(calculatedColumnWidth);
|
||||
},
|
||||
onDragStart: () => {
|
||||
state.update('isDragging', true);
|
||||
const dragElement = dragElementRef?.current;
|
||||
const parentElement = wrapperElementRef?.current;
|
||||
const columnWidthPrevious =
|
||||
!dragElement || !parentElement ? state.columnWidth : dragElement.offsetLeft - parentElement.offsetLeft;
|
||||
state.update('columnWidthPrevious', columnWidthPrevious);
|
||||
},
|
||||
onDragEnd: () => {
|
||||
state.update('isDragging', false);
|
||||
state.update('columnWidthPrevious', state.columnWidth);
|
||||
if (!state.collapsedFixed && !state.collapsedFluid) {
|
||||
state.update('columnWidthStorage', state.columnWidth);
|
||||
}
|
||||
},
|
||||
onDoubleClick: () => {
|
||||
state.reset(columnWidthDefault);
|
||||
},
|
||||
});
|
||||
|
||||
const gridTemplateColumns = state.collapsedFixed
|
||||
? `auto auto 1fr`
|
||||
: state.collapsedFluid
|
||||
? `1fr auto auto`
|
||||
: `${state.columnWidth}px auto 1fr`;
|
||||
|
||||
return (
|
||||
<div {...props} ref={wrapperElementRef} css={wrapperStyle} style={{ gridTemplateColumns }}>
|
||||
{/* we need to keep these components mounted, so use hidden={isCollapsed} */}
|
||||
<GridCell hidden={!state.collapsedFixed}>{fixedCollapsedContent(state.commandProps)}</GridCell>
|
||||
<GridCell hidden={state.collapsedFixed}>{fixedContent(state.commandProps)}</GridCell>
|
||||
<div ref={dragElementRef} css={draggerWrapperStyle} {...bind()}>
|
||||
<DraggerComponent isDragging={state.isDragging} />
|
||||
</div>
|
||||
<GridCell hidden={!state.collapsedFluid}>{fluidCollapsedContent(state.commandProps)}</GridCell>
|
||||
<GridCell hidden={state.collapsedFluid}>{fluidContent(state.commandProps)}</GridCell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const columnWidthId = 'columnWidth';
|
||||
const getColumnWidthStorage = (defaultWidth = 0) =>
|
||||
parseInt(window.localStorage.getItem(columnWidthId) || defaultWidth.toString(), 10);
|
||||
|
||||
const wrapperStyle = css`
|
||||
display: grid;
|
||||
grid-template-columns: 500px 10px 1fr;
|
||||
`;
|
||||
const draggerWrapperStyle = css`
|
||||
display: grid;
|
||||
z-index: 2;
|
||||
`;
|
||||
const DefaultFluidCollapsedComponent = styled.div`
|
||||
min-width: 4rem;
|
||||
`;
|
||||
const DefaultFixedCollapsedComponent = styled.div`
|
||||
min-width: 4rem;
|
||||
`;
|
||||
const GridCell = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export type DraggerRendererProps = ComponentProps<'div'> & {
|
||||
isDragging: boolean;
|
||||
};
|
||||
export const DefaultDraggerComponent: FC<DraggerRendererProps> = ({ isDragging, ...props }) => (
|
||||
<div css={[draggerStyle, isDragging ? isDraggingStyle : undefined]} {...props}>
|
||||
<DefaultDraggerHandle isDragging={isDragging} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const draggerStyle = css`
|
||||
width: 1px;
|
||||
margin: 0 -1px;
|
||||
cursor: col-resize;
|
||||
display: flex;
|
||||
position: relative;
|
||||
&:before {
|
||||
content: '';
|
||||
background-color: ${CoreTokens.BorderNormal};
|
||||
width: 5px;
|
||||
margin: 0 -2px;
|
||||
opacity: 0;
|
||||
border: 1px solid ${CoreTokens.BorderNormal};
|
||||
}
|
||||
&:hover:before {
|
||||
opacity: 0.3;
|
||||
}
|
||||
`;
|
||||
const isDraggingStyle = css`
|
||||
&,
|
||||
&:before,
|
||||
&:hover:before,
|
||||
&:active:before {
|
||||
background-color: ${CoreTokens.Intent.Primary3};
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export type DraggerHandleProps = ComponentProps<'div'> & {
|
||||
isDragging?: boolean;
|
||||
};
|
||||
export const DefaultDraggerHandle: FC<DraggerHandleProps> = ({ isDragging, ...props }) => (
|
||||
<div css={[handleStyle, isDragging ? handleStyleIsDraggingStyle : undefined]} {...props}>
|
||||
<DragCircle />
|
||||
<DragCircle />
|
||||
<DragCircle />
|
||||
</div>
|
||||
);
|
||||
const handleStyle = css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 99px;
|
||||
border: 1px solid ${CoreTokens.BorderNormal};
|
||||
min-height: 23px;
|
||||
max-height: 23px;
|
||||
min-width: 9px;
|
||||
max-width: 9px;
|
||||
margin: 0.75rem -4px;
|
||||
color: ${CoreTokens.TextIcon};
|
||||
background-color: ${CoreTokens.Background1};
|
||||
`;
|
||||
const handleStyleIsDraggingStyle = css`
|
||||
background-color: ${CoreTokens.Intent.Primary3};
|
||||
color: ${CoreTokens.OnIntent};
|
||||
`;
|
||||
const DragCircle = styled.div`
|
||||
border-radius: 99px;
|
||||
background-color: currentColor;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: 1px;
|
||||
`;
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Intent, MaybeElement } from '@blueprintjs/core';
|
||||
import { Button, MenuItem } from '@blueprintjs/core';
|
||||
import type { ItemRenderer } from '@blueprintjs/select';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import { css } from '@emotion/react';
|
||||
import { Txt } from '@redeye/ui-styles';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const renderSort: ItemRenderer<{ key: string; label: string }> = (item, { handleClick, modifiers }) => {
|
||||
if (!modifiers.matchesPredicate) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
css={css`
|
||||
text-transform: capitalize;
|
||||
`}
|
||||
active={modifiers.active}
|
||||
key={item.key}
|
||||
onClick={handleClick}
|
||||
text={item.label}
|
||||
cy-test={item.label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type DropdownItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
enumKey: any;
|
||||
};
|
||||
|
||||
interface DropdownProps {
|
||||
items: DropdownItem[];
|
||||
activeItem?: DropdownItem;
|
||||
onSelect: (a: DropdownItem) => unknown;
|
||||
text?: React.ReactNode;
|
||||
labelText?: React.ReactNode;
|
||||
defaultText?: React.ReactNode;
|
||||
['cy-test']: string;
|
||||
className?: string;
|
||||
intent?: Intent;
|
||||
icon?: MaybeElement;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Dropdown: FC<DropdownProps> = ({
|
||||
items,
|
||||
activeItem,
|
||||
onSelect,
|
||||
text,
|
||||
labelText,
|
||||
defaultText,
|
||||
'cy-test': cyTest,
|
||||
className,
|
||||
intent,
|
||||
icon,
|
||||
disabled,
|
||||
}) => (
|
||||
<Select
|
||||
popoverProps={{ minimal: true }}
|
||||
filterable={false}
|
||||
activeItem={activeItem}
|
||||
itemRenderer={renderSort}
|
||||
items={items}
|
||||
onItemSelect={onSelect}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button
|
||||
cy-test={cyTest}
|
||||
text={
|
||||
<span>
|
||||
<Txt bold>{labelText}</Txt> <Txt css={valueStyle}>{text || defaultText}</Txt>
|
||||
</span>
|
||||
}
|
||||
minimal
|
||||
className={className}
|
||||
intent={intent}
|
||||
rightIcon={icon}
|
||||
disabled={disabled}
|
||||
small
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const valueStyle = css`
|
||||
text-transform: capitalize;
|
||||
/* margin-left: 0.5rem; */
|
||||
`;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Button, NonIdealState } from '@blueprintjs/core';
|
||||
import { Warning16 } from '@carbon/icons-react';
|
||||
import { css } from '@emotion/react';
|
||||
import { CarbonIcon } from '@redeye/client/components';
|
||||
import { CoreTokens } from '@redeye/ui-styles';
|
||||
import { useCallback } from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||
const updateClipboard = useCallback(() => {
|
||||
window.navigator.clipboard
|
||||
.writeText(JSON.stringify({ error: error.name, message: error.message, stack: error.stack }))
|
||||
.then(
|
||||
() => {
|
||||
/* clipboard successfully set */
|
||||
},
|
||||
() => {
|
||||
/* clipboard write failed */
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
return (
|
||||
<NonIdealState
|
||||
icon={
|
||||
<CarbonIcon
|
||||
icon={Warning16}
|
||||
css={css`
|
||||
color: ${CoreTokens.TextIntentDanger};
|
||||
|
||||
& svg {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
}
|
||||
title="An Error Occurred"
|
||||
description={error.message}
|
||||
action={
|
||||
<>
|
||||
<Button onClick={updateClipboard}>Copy error to clipboard</Button>
|
||||
<Button onClick={resetErrorBoundary}>Try again</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { HTMLInputProps, InputGroupProps } from '@blueprintjs/core';
|
||||
import { Button, Classes, InputGroup } from '@blueprintjs/core';
|
||||
import { Close16, Search16 } from '@carbon/icons-react';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { CarbonIcon } from '@redeye/client/components';
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
|
||||
type ExpandingSearchBoxProps = InputGroupProps &
|
||||
HTMLInputProps & {
|
||||
onClear?: (event: MouseEvent<HTMLElement>) => void;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export const ExpandingSearchBox: FC<ExpandingSearchBoxProps> = ({ onClear, ...props }) => {
|
||||
const hasText = props.value !== '';
|
||||
return (
|
||||
<CollapseDiv className={hasText ? stayOpenClass : undefined}>
|
||||
<InputGroup
|
||||
{...props}
|
||||
// type="search" // this adds the silly browser x icon button
|
||||
aria-label="search"
|
||||
leftIcon={<CarbonIcon icon={Search16} />}
|
||||
rightElement={
|
||||
<Button
|
||||
icon={<CarbonIcon icon={Close16} />}
|
||||
minimal
|
||||
small
|
||||
onClick={onClear}
|
||||
disabled={!hasText}
|
||||
css={css`
|
||||
&:disabled {
|
||||
cursor: unset !important;
|
||||
}
|
||||
`} // remove the cursor:not-allowed;
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</CollapseDiv>
|
||||
);
|
||||
};
|
||||
|
||||
const stayOpenClass = 'stayOpen';
|
||||
const CollapseDiv = styled.div`
|
||||
overflow: hidden; /* hide the translated input */
|
||||
transition: 100ms ease;
|
||||
|
||||
& .${Classes.INPUT_GROUP} {
|
||||
transition: inherit;
|
||||
transition-property: transform;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
& .${Classes.INPUT} {
|
||||
transition: inherit;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
&:not(:hover):not(:focus-within):not(.${stayOpenClass}) {
|
||||
.${Classes.INPUT} {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.${Classes.INPUT_GROUP} {
|
||||
transform: translate(calc(100% - 3rem), 0);
|
||||
transition-property: transform;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Button, Callout, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import { ArrowRight16, Password16, Warning20 } from '@carbon/icons-react';
|
||||
import { css } from '@emotion/react';
|
||||
import { CarbonIcon, UsernameInput } from '@redeye/client/components';
|
||||
import { useStore } from '@redeye/client/store';
|
||||
import { routes } from '@redeye/client/store/routing/router';
|
||||
import { Views } from '@redeye/client/types';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { ComponentProps, FormEvent } from 'react';
|
||||
import { createState } from '../mobx-create-state';
|
||||
|
||||
type LoginFormProps = ComponentProps<'form'>;
|
||||
|
||||
export const LoginForm = observer<LoginFormProps>(({ ...props }) => {
|
||||
const store = useStore();
|
||||
const state = createState({
|
||||
username: store.auth.userName || '',
|
||||
password: '',
|
||||
passwordFocus: false,
|
||||
loading: false,
|
||||
errorMessage: '',
|
||||
*handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
this.loading = true;
|
||||
if (store.appMeta.blueTeam) {
|
||||
store.auth.setUser(this.username);
|
||||
store.router.updateRoute({ path: routes[Views.CAMPAIGNS_LIST], params: { id: 'all' } });
|
||||
} else {
|
||||
// Make login call
|
||||
const formData = new FormData();
|
||||
formData.append('password', this.password);
|
||||
try {
|
||||
const loginResponse: Response = yield fetch(`${store.auth.serverUrl}/api/login`, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
});
|
||||
// TODO: could set a state that makes the password and server inputs invalid?
|
||||
this.loading = false;
|
||||
if (loginResponse.status === 401) {
|
||||
if (!this.password) this.errorMessage = 'Password Required';
|
||||
else this.errorMessage = 'Incorrect password';
|
||||
} else if (loginResponse.status === 400 || loginResponse.status > 401)
|
||||
this.errorMessage = 'Error communicating with server';
|
||||
else if (loginResponse.status !== 200) this.errorMessage = 'Error logging in';
|
||||
else {
|
||||
this.errorMessage = '';
|
||||
if (this.username !== store.auth.userName) {
|
||||
window.localStorage.removeItem('pendingComments');
|
||||
}
|
||||
store.auth.setUser(this.username);
|
||||
store.router.updateRoute({ path: routes[Views.CAMPAIGNS_LIST], params: { id: 'all' } });
|
||||
}
|
||||
} catch (e) {
|
||||
this.loading = false;
|
||||
this.errorMessage = 'Error communicating with server';
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { data, refetch } = useQuery(
|
||||
['users', state.password],
|
||||
async () => await store.graphqlStore.queryGlobalOperators({ password: state.password }),
|
||||
{
|
||||
enabled: !!state.password && !state.passwordFocus,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<form cy-test="login-form" {...props} onSubmit={state.handleSubmit} autoComplete="on">
|
||||
{!store.appMeta.blueTeam && (
|
||||
<>
|
||||
<InputGroup
|
||||
value={state.password}
|
||||
onChange={(e) => state.update('password', e.target.value)}
|
||||
cy-test="password"
|
||||
autoComplete="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="server password"
|
||||
css={inputSpacingTightStyle}
|
||||
onFocus={() => state.update('passwordFocus', true)}
|
||||
onBlur={() => state.update('passwordFocus', false)}
|
||||
leftIcon={<CarbonIcon icon={Password16} />}
|
||||
large
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<UsernameInput
|
||||
cy-test="username"
|
||||
username={state.username}
|
||||
password={state.password}
|
||||
refetch={refetch}
|
||||
softDisable={!data}
|
||||
users={data?.globalOperators}
|
||||
onUsernameUpdate={(username) => state.update('username', username)}
|
||||
css={otherSpacingLooseStyle}
|
||||
/>
|
||||
{!!state.errorMessage && (
|
||||
<Callout
|
||||
css={otherSpacingLooseStyle}
|
||||
intent={Intent.DANGER}
|
||||
icon={<CarbonIcon icon={Warning20} />}
|
||||
children={state.errorMessage}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
cy-test="login-btn"
|
||||
text="Login"
|
||||
loading={state.loading}
|
||||
intent="primary"
|
||||
css={otherSpacingLooseStyle}
|
||||
disabled={state.username.length < 1 || (state.password.length < 1 && !store.appMeta.blueTeam)}
|
||||
type="submit"
|
||||
rightIcon={<CarbonIcon icon={ArrowRight16} />}
|
||||
large
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
const inputSpacingTightStyle = css`
|
||||
margin-bottom: 0.5rem;
|
||||
`;
|
||||
const otherSpacingLooseStyle = css`
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Classes, FormGroup, Switch } from '@blueprintjs/core';
|
||||
import { TimezoneSelect } from '@blueprintjs/datetime';
|
||||
import { css } from '@emotion/react';
|
||||
import { SortDirection, useStore } from '@redeye/client/store';
|
||||
import { sortOptions } from '@redeye/client/views';
|
||||
import { Txt, unRedactedFontClassName } from '@redeye/ui-styles';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { ChangeEvent, ComponentProps } from 'react';
|
||||
import { CampaignViews, Tabs } from '../../types';
|
||||
import { createState } from '../mobx-create-state';
|
||||
|
||||
type SettingsFormProps = ComponentProps<'form'> & {};
|
||||
|
||||
export const SettingsForm = observer<SettingsFormProps>(({ ...props }) => {
|
||||
const store = useStore();
|
||||
const state = createState({
|
||||
tester: { value: 3 },
|
||||
enableAutoSelect: store.settings.isDefaultTimezone,
|
||||
setEnableAutoSelect(e: ChangeEvent<HTMLInputElement>) {
|
||||
this.enableAutoSelect = e.target.checked;
|
||||
return e.target.checked && store.settings.setDefaultTimezone();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form css={formStyles} {...props}>
|
||||
{/* <Txt small>Timezone</Txt> */}
|
||||
|
||||
<FormGroup
|
||||
label="Timezone"
|
||||
helperText={
|
||||
<Switch
|
||||
// inline
|
||||
// alignIndicator="right"
|
||||
checked={state.enableAutoSelect}
|
||||
onChange={state.setEnableAutoSelect}
|
||||
label="AutoSelect"
|
||||
css={{ marginBottom: 0 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TimezoneSelect
|
||||
css={timezonePickerStyle}
|
||||
buttonProps={{ fill: true, alignText: 'left' }}
|
||||
disabled={state.enableAutoSelect}
|
||||
value={store.settings.timezone}
|
||||
// showLocalTimezone
|
||||
onChange={(timezone) => {
|
||||
store.settings.setTimezone(timezone);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Switch
|
||||
cy-test="show-hide-beacons"
|
||||
checked={store.settings.showHidden}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
store.settings.setShowHidden(e.target.checked);
|
||||
store.reset(false);
|
||||
store.campaign.refetch();
|
||||
store.campaign.search.clearSearch();
|
||||
store.router.updateRoute({
|
||||
path: store.router.currentRoute,
|
||||
params: {
|
||||
id: store.campaign.id!,
|
||||
view: store.router.params.view,
|
||||
...(store.router.params.view === CampaignViews.EXPLORE
|
||||
? { currentItem: 'all', currentItemId: undefined, tab: Tabs.HOSTS }
|
||||
: {}),
|
||||
},
|
||||
queryParams:
|
||||
store.router.params.view === CampaignViews.EXPLORE
|
||||
? { sort: `${sortOptions[Tabs.HOSTS][0].key} ${SortDirection.ASC}` }
|
||||
: {},
|
||||
clear: true,
|
||||
});
|
||||
}}
|
||||
label="Show Hidden Beacons, Hosts, and Servers"
|
||||
/>
|
||||
<Switch
|
||||
cy-test="toggle-theme"
|
||||
checked={store.settings.theme === 'light'}
|
||||
onChange={(event) => store.settings.setTheme(event.currentTarget.checked ? 'light' : 'dark')}
|
||||
label="Light Theme (beta)"
|
||||
/>
|
||||
<Switch
|
||||
cy-test="toggle-redacted-mode"
|
||||
checked={store.settings.redactedMode}
|
||||
onChange={(event) => store.settings.setRedactedMode(event.currentTarget.checked)}
|
||||
className={unRedactedFontClassName}
|
||||
// @ts-ignore // `label` prop actually supports JSX elements, but TypeScript will throw an error because HTMLAttributes only allows strings.
|
||||
label={
|
||||
<span>
|
||||
<Txt>Redacted Screenshot Mode</Txt>
|
||||
<br />
|
||||
<Txt small muted>
|
||||
WARNING: Beacon, Host, and Server names may still be visible in url
|
||||
</Txt>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
const formStyles = css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
& > * {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const timezonePickerStyle = css`
|
||||
.${Classes.POPOVER_TARGET} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Classes, MenuItem } from '@blueprintjs/core';
|
||||
import type { ItemPredicate, SuggestProps } from '@blueprintjs/select';
|
||||
import { Suggest, getCreateNewItem } from '@blueprintjs/select';
|
||||
import { User16, UserFollow16 } from '@carbon/icons-react';
|
||||
import { css } from '@emotion/react';
|
||||
import { CarbonIcon, createState, escapeRegExpChars } from '@redeye/client/components';
|
||||
import type { GlobalOperatorModel } from '@redeye/client/store';
|
||||
import { useStore } from '@redeye/client/store';
|
||||
import { UtilityStyles, Txt, CoreTokens } from '@redeye/ui-styles';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type UsernameInputProps = Omit<
|
||||
SuggestProps<GlobalOperatorModel>,
|
||||
'itemRenderer' | 'items' | 'inputValueRenderer' | 'onItemSelect'
|
||||
> & {
|
||||
username: string;
|
||||
password: string;
|
||||
softDisable: boolean;
|
||||
refetch: () => any;
|
||||
users?: GlobalOperatorModel[];
|
||||
onUsernameUpdate: (username) => void;
|
||||
};
|
||||
|
||||
export const UsernameInput = observer<UsernameInputProps>(
|
||||
({ username, password, softDisable, refetch, users = [], onUsernameUpdate, ...props }) => {
|
||||
const store = useStore();
|
||||
|
||||
const state = createState({
|
||||
activeItem: null as null | GlobalOperatorModel,
|
||||
query: username,
|
||||
});
|
||||
|
||||
const { mutate: addUser } = useMutation(
|
||||
async () =>
|
||||
await store.graphqlStore.mutateCreateGlobalOperator({
|
||||
username: state.query,
|
||||
password,
|
||||
}),
|
||||
{
|
||||
onSuccess(op) {
|
||||
if (op?.createGlobalOperator) {
|
||||
refetch();
|
||||
onUsernameUpdate(state.query);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Suggest
|
||||
cy-test="username"
|
||||
query={state.query}
|
||||
createNewItemFromQuery={(query) => ({ name: query, id: query } as any)}
|
||||
selectedItem={
|
||||
store.graphqlStore.globalOperators.get(state.query) ||
|
||||
({ name: state.query, id: state.query } as GlobalOperatorModel)
|
||||
}
|
||||
itemPredicate={filterUsers}
|
||||
activeItem={state.activeItem || getCreateNewItem()}
|
||||
onItemSelect={(item) => {
|
||||
state.update('query', item?.name);
|
||||
onUsernameUpdate(item.name);
|
||||
}}
|
||||
onActiveItemChange={(activeItem) => {
|
||||
state.update('activeItem', activeItem);
|
||||
// if there is a plausible matching username, use it.
|
||||
// otherwise, used the typed text
|
||||
onUsernameUpdate(activeItem ? activeItem.name : state.query);
|
||||
}}
|
||||
onQueryChange={(query) => {
|
||||
state.update('query', query);
|
||||
}}
|
||||
items={users || []}
|
||||
inputValueRenderer={(item) => item.name as string}
|
||||
css={menuParentStyle}
|
||||
fill
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
matchTargetWidth: true,
|
||||
}}
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
autoComplete: 'username',
|
||||
placeholder: 'user',
|
||||
leftIcon: <CarbonIcon icon={User16} />,
|
||||
large: true,
|
||||
}}
|
||||
itemRenderer={(user, { handleClick, modifiers }) => {
|
||||
if (!modifiers.matchesPredicate) return null;
|
||||
return (
|
||||
<MenuItem
|
||||
text={highlightText(user.name as string, state.query)}
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
// label={user.campaign} // TODO: add campaign the user comes from
|
||||
key={user.name}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
createNewItemRenderer={
|
||||
store.appMeta.blueTeam
|
||||
? undefined
|
||||
: (_, isActive: boolean) =>
|
||||
softDisable ? (
|
||||
<div css={{ padding: '4px 12px' }}>
|
||||
<Txt small bold block css={password && { color: CoreTokens.TextIntentDanger }}>
|
||||
{password ? 'Invalid Server Password' : 'No Server Password'}
|
||||
</Txt>
|
||||
<Txt small italic muted block>
|
||||
Enter a valid password to see user options
|
||||
</Txt>
|
||||
</div>
|
||||
) : (
|
||||
<MenuItem
|
||||
icon={<CarbonIcon icon={UserFollow16} />}
|
||||
text={state.query}
|
||||
disabled={store.graphqlStore.globalOperators.has(state.query)}
|
||||
label="Add new user"
|
||||
active={isActive}
|
||||
onClick={() => addUser()}
|
||||
shouldDismissPopover={false}
|
||||
intent="primary"
|
||||
css={newUserStyle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const menuParentStyle = css`
|
||||
.${Classes.MENU} {
|
||||
min-width: 240px;
|
||||
}
|
||||
`;
|
||||
|
||||
const newUserStyle = css`
|
||||
border-top: 1px solid ${CoreTokens.BorderMuted};
|
||||
`;
|
||||
|
||||
function highlightText(text: string, query: string) {
|
||||
let lastIndex = 0;
|
||||
const words = query
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 0)
|
||||
.map(escapeRegExpChars);
|
||||
if (words.length === 0) {
|
||||
return [text];
|
||||
}
|
||||
const regexp = new RegExp(words.join('|'), 'gi');
|
||||
const tokens: React.ReactNode[] = [];
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const match = regexp.exec(text);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
const length = match[0].length;
|
||||
const before = text.slice(lastIndex, regexp.lastIndex - length);
|
||||
if (before.length > 0) {
|
||||
tokens.push(before);
|
||||
}
|
||||
lastIndex = regexp.lastIndex;
|
||||
tokens.push(
|
||||
<Txt css={UtilityStyles.textHighlight} key={lastIndex}>
|
||||
{match[0]}
|
||||
</Txt>
|
||||
);
|
||||
}
|
||||
const rest = text.slice(lastIndex);
|
||||
if (rest.length > 0) {
|
||||
tokens.push(rest);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export const filterUsers: ItemPredicate<GlobalOperatorModel> = (query, user, _index, exactMatch) => {
|
||||
const normalizedTitle = user.name?.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return !!normalizedTitle?.includes(normalizedQuery);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from './LoginForm';
|
||||
export * from './SettingsForm';
|
||||
export * from './UsernameInput';
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { ButtonGroupProps } from '@blueprintjs/core';
|
||||
import { Button, ButtonGroup } from '@blueprintjs/core';
|
||||
import { Help16, Help24, Settings16, Settings24, User16, User24 } from '@carbon/icons-react';
|
||||
import {
|
||||
CarbonIcon,
|
||||
GeneralSettingsOverlay,
|
||||
HelpOverlay,
|
||||
ModeIndicator,
|
||||
NavButton,
|
||||
UserSettingsOverlay,
|
||||
} from '@redeye/client/components';
|
||||
import type { AppStore } from '@redeye/client/store';
|
||||
import { useStore } from '@redeye/client/store';
|
||||
import { Border } from '@redeye/ui-styles';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { UseCreateState } from '../mobx-create-state';
|
||||
import { createState } from '../mobx-create-state';
|
||||
|
||||
type SettingsOverlayState = {
|
||||
isUserSettingsOpen: boolean;
|
||||
isHelpPanelOpen: boolean;
|
||||
isGeneralSettingsOpen: boolean;
|
||||
};
|
||||
|
||||
type AppOptionsProps = Omit<ButtonGroupProps, 'children'> & {
|
||||
condensed?: boolean;
|
||||
navBar?: boolean;
|
||||
};
|
||||
|
||||
export const AppOptions = observer<AppOptionsProps>(({ condensed = false, navBar, ...props }) => {
|
||||
const state = createState<SettingsOverlayState>({
|
||||
isUserSettingsOpen: false,
|
||||
isHelpPanelOpen: false,
|
||||
isGeneralSettingsOpen: false,
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
return (
|
||||
<>
|
||||
{navBar ? (
|
||||
<NavBarOptions state={state} />
|
||||
) : (
|
||||
<HeaderOptions state={state} store={store} condensed={condensed} {...props} />
|
||||
)}
|
||||
<UserSettingsOverlay
|
||||
isOpen={state.isUserSettingsOpen}
|
||||
onClose={() => {
|
||||
state.update('isUserSettingsOpen', false);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
state.update('isUserSettingsOpen', false);
|
||||
}}
|
||||
/>
|
||||
<GeneralSettingsOverlay
|
||||
isOpen={state.isGeneralSettingsOpen}
|
||||
onClose={() => {
|
||||
state.update('isGeneralSettingsOpen', false);
|
||||
}}
|
||||
/>
|
||||
<HelpOverlay
|
||||
isOpen={state.isHelpPanelOpen}
|
||||
onClose={() => {
|
||||
state.update('isHelpPanelOpen', false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const HeaderOptions = ({
|
||||
state,
|
||||
store,
|
||||
condensed,
|
||||
...props
|
||||
}: {
|
||||
state: UseCreateState<SettingsOverlayState>;
|
||||
store: AppStore;
|
||||
condensed: boolean;
|
||||
}) => (
|
||||
<ButtonGroup {...props} minimal>
|
||||
<Button
|
||||
cy-test="user"
|
||||
text={store.auth.userName}
|
||||
onClick={() => {
|
||||
state.update('isUserSettingsOpen', true);
|
||||
}}
|
||||
rightIcon={<CarbonIcon icon={User16} />}
|
||||
/>
|
||||
<Button
|
||||
cy-test="settings"
|
||||
text={condensed ? undefined : 'Settings'}
|
||||
onClick={() => {
|
||||
state.update('isGeneralSettingsOpen', true);
|
||||
}}
|
||||
rightIcon={<CarbonIcon icon={Settings16} />}
|
||||
/>
|
||||
<Button
|
||||
cy-test="help"
|
||||
text={condensed ? undefined : 'Help'}
|
||||
onClick={() => {
|
||||
state.update('isHelpPanelOpen', true);
|
||||
}}
|
||||
rightIcon={<CarbonIcon icon={Help16} />}
|
||||
/>
|
||||
<ModeIndicator fullName css={{ marginRight: -8, padding: '0px 8px' }} popoverProps={{ position: 'top-right' }} />
|
||||
</ButtonGroup>
|
||||
);
|
||||
|
||||
const NavBarOptions = ({ state }: { state: UseCreateState<SettingsOverlayState> }) => (
|
||||
<>
|
||||
<NavButton
|
||||
cy-test="user-settings"
|
||||
icon={<CarbonIcon icon={User24} />}
|
||||
title="User Settings"
|
||||
onClick={() => {
|
||||
state.update('isUserSettingsOpen', true);
|
||||
}}
|
||||
active={state.isUserSettingsOpen}
|
||||
/>
|
||||
<NavButton
|
||||
cy-test="settings"
|
||||
icon={<CarbonIcon icon={Settings24} />}
|
||||
title="General Settings"
|
||||
onClick={() => {
|
||||
state.update('isGeneralSettingsOpen', true);
|
||||
}}
|
||||
active={state.isGeneralSettingsOpen}
|
||||
/>
|
||||
<NavButton
|
||||
cy-test="help-btn"
|
||||
icon={<CarbonIcon icon={Help24} />}
|
||||
title="Help"
|
||||
onClick={() => {
|
||||
state.update('isHelpPanelOpen', true);
|
||||
}}
|
||||
active={state.isHelpPanelOpen}
|
||||
/>
|
||||
<Border css={{ margin: '16px 0', width: 16 }} invert />
|
||||
<ModeIndicator css={{ alignSelf: 'stretch', height: 48 }} />
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { css } from '@emotion/react';
|
||||
import { Logo } from '@redeye/client/components';
|
||||
import { Header, Txt } from '@redeye/ui-styles';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
|
||||
type AppTitleProps = ComponentProps<'div'> & {};
|
||||
|
||||
const appTitle = 'RedEye'; //
|
||||
const appSubtitle = 'Red Team C2 Log Visualization';
|
||||
|
||||
const logoBaselinePosition = Math.round((99 / 159) * 100) / 100;
|
||||
const logoSize = 200;
|
||||
const logoSizeSmall = 160;
|
||||
|
||||
export const AppTitle: FC<AppTitleProps> = ({ ...props }) => (
|
||||
<div
|
||||
{...props}
|
||||
css={css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<Header
|
||||
h={1}
|
||||
css={css`
|
||||
font-size: 3.5rem !important;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-left: -3px;
|
||||
`}
|
||||
cy-test="appTitle"
|
||||
>
|
||||
<Logo
|
||||
css={css`
|
||||
margin-top: -${logoBaselinePosition * logoSize}px;
|
||||
margin-bottom: -${(1 - logoBaselinePosition) * logoSize - 1}px;
|
||||
margin-left: -${logoSize}px;
|
||||
height: ${logoSize}px;
|
||||
width: ${logoSize}px;
|
||||
transform: translateX(-2rem);
|
||||
@media (max-width: 64rem) {
|
||||
height: ${logoSizeSmall}px;
|
||||
width: ${logoSizeSmall}px;
|
||||
position: static;
|
||||
display: block;
|
||||
transform: none;
|
||||
/* margin: -3rem 1.5rem -1rem -2.5rem; */
|
||||
margin: 0;
|
||||
margin-left: -${logoSizeSmall * 0.25}px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<Txt>{appTitle}</Txt>
|
||||
</Header>
|
||||
<Txt muted small meta>
|
||||
{appSubtitle}
|
||||
</Txt>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { css } from '@emotion/react';
|
||||
import { AppOptions, AppTitle } from '@redeye/client/components';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
|
||||
type HeaderProps = ComponentProps<'div'> & {};
|
||||
|
||||
export const AppHeader: FC<HeaderProps> = ({ ...props }) => (
|
||||
<header
|
||||
css={css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<AppTitle css={{ marginBottom: '0.5rem' }} />
|
||||
<AppOptions />
|
||||
</header>
|
||||
);
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_13" data-name="Layer 13" xmlns="http://www.w3.org/2000/svg" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#linear-gradient-6);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#linear-gradient-3);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: url(#linear-gradient-4);
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: url(#linear-gradient-5);
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: url(#radial-gradient);
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
opacity: .15;
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
opacity: .3;
|
||||
}
|
||||
</style>
|
||||
<radialGradient id="radial-gradient" cx="134.8386" cy="116.6086" fx="134.8386" fy="116.6086" r="40.159" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="red"/>
|
||||
<stop offset=".1498" stop-color="#f90000"/>
|
||||
<stop offset=".3381" stop-color="#ea0000"/>
|
||||
<stop offset=".547" stop-color="#d00000"/>
|
||||
<stop offset=".7707" stop-color="#ac0000"/>
|
||||
<stop offset="1" stop-color="maroon"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="linear-gradient" x1="178.2987" y1="45.0278" x2="2.1541" y2="133.8138" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".3982" stop-color="#ccc"/>
|
||||
<stop offset=".4986" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#4d4d4d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-2" x1="13.2721" y1="94.4971" x2="30.793" y2="133.2128" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".001" stop-color="#fff"/>
|
||||
<stop offset="1" stop-color="#999"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-3" x1="169.9339" y1="109.8548" x2="182.0855" y2="80.1822" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".001" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#666"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-4" x1="240.239" y1="118.1809" x2="74.372" y2="216.1024" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".4695" stop-color="#ccc"/>
|
||||
<stop offset=".5872" stop-color="#999"/>
|
||||
<stop offset="1" stop-color="#4d4d4d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-5" x1="241.8381" y1="160.4242" x2="225.4475" y2="122.8389" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".012" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#666"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-6" x1="85.7089" y1="145.4674" x2="74.9702" y2="173.7271" xlink:href="#linear-gradient-2"/>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-7" d="M139.7025,104h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.031l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.031l-11.6667-20c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845Z"/>
|
||||
<g class="cls-9">
|
||||
<path class="cls-6" d="M139.7025,105c1.0629,0,2.0558,.5703,2.5913,1.4883l11.6667,20c.5439,.9324,.5439,2.0909,0,3.0233l-11.6667,20c-.5355,.918-1.5285,1.4883-2.5913,1.4883h-23.405c-1.0629,0-2.0558-.5703-2.5913-1.4883l-11.6667-20c-.5439-.9324-.5439-2.0909,0-3.0233l11.6667-20c.5355-.918,1.5285-1.4883,2.5913-1.4883h23.405m0-1h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.031l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.031l-11.6667-20c-.7169-1.2289-2.0324-1.9845-3.4551-1.9845h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g class="cls-11">
|
||||
<path class="cls-6" d="M130.2975,128h22.2204c1.5436,0,2.5053-1.6744,1.7276-3.0077l-11.0878-19.0077c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845h-22.2204c-1.5436,0-2.5053,1.6744-1.7276,3.0077l11.0878,19.0077c.7168,1.2289,2.0324,1.9845,3.4551,1.9845Z"/>
|
||||
</g>
|
||||
<g class="cls-10">
|
||||
<path d="M126.8243,130.0155l-11.0967,19.023c-.7718,1.323-2.6834,1.323-3.4551,0l-11.0967-19.023c-.7265-1.2455-.7265-2.7855,0-4.031l11.0967-19.023c.7718-1.323,2.6834-1.323,3.4551,0l11.0967,19.023c.7265,1.2455,.7265,2.7855,0,4.031Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-8" d="M208.86,84.2906l-25.551-44.2884c-1.4287-2.4765-4.0704-4.0022-6.9295-4.0022H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9768l30.3695-52.0619c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path class="cls-6" d="M176.3795,37c2.4938,0,4.8171,1.3419,6.0633,3.5019l25.551,44.2884c.4199,.7278,.5161,1.5713,.2708,2.3749-.2453,.8036-.796,1.4496-1.5507,1.819l-42.3075,20.7036-12.5549-22.1804c-.1775-.3136-.5099-.5074-.8703-.5074h-40.2375c-3.8971,0-7.5379,2.0912-9.5016,5.4575l-30.3694,52.0619c-.8926,1.5302-2.5475,2.4807-4.3189,2.4807H13.565c-1.5243,0-2.8055-1.1419-2.9802-2.6561l-5.4628-47.3439H40c.3557,0,.6846-.1889,.8638-.4962l32.1059-55.0387c1.6066-2.7542,4.5855-4.4651,7.774-4.4651h95.6358m0-1H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9767l30.3694-52.062c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5918l-25.551-44.2884c-1.4287-2.4764-4.0704-4.0022-6.9295-4.0022h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-3" d="M9.4796,143.4895l-5.4796-47.4895H40l-27.731,48.0672c-.7224,1.2521-2.6237,.8584-2.7894-.5776Z"/>
|
||||
<path class="cls-2" d="M164,111l41.1816-20.1527c1.428-.6988,.9305-2.8473-.6593-2.8473h-53.5222l13,23Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-4" d="M47.14,171.7094l25.551,44.2884c1.4287,2.4765,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9768l-30.3695,52.0619c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path class="cls-6" d="M242.435,109c1.5243,0,2.8055,1.1419,2.9802,2.6561l5.4628,47.3439h-34.878c-.3557,0-.6846,.1889-.8638,.4962l-32.1059,55.0387c-1.6066,2.7542-4.5855,4.4651-7.774,4.4651H79.6205c-2.4938,0-4.8171-1.3419-6.0633-3.5019l-25.551-44.2884c-.4199-.7278-.5161-1.5713-.2708-2.3749,.2453-.8036,.796-1.4496,1.5507-1.819l42.3075-20.7036,12.5549,22.1804c.1775,.3136,.5099,.5074,.8702,.5074h40.2375c3.8971,0,7.5379-2.0912,9.5016-5.4575l30.3694-52.0619c.8926-1.5302,2.5475-2.4807,4.3189-2.4807h52.9888m0-1h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9767l-30.3694,52.062c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918l25.551,44.2884c1.4287,2.4764,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-5" d="M246.5204,112.5105l5.4796,47.4895h-36l27.731-48.0672c.7224-1.2521,2.6237-.8584,2.7894,.5776Z"/>
|
||||
<path class="cls-1" d="M92,145l-41.1816,20.1527c-1.428,.6988-.9305,2.8473,.6593,2.8473h53.5222l-13-23Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_13" data-name="Layer 13" xmlns="http://www.w3.org/2000/svg" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#linear-gradient-6);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#linear-gradient-3);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: url(#linear-gradient-4);
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: url(#linear-gradient-5);
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: url(#radial-gradient);
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
opacity: .15;
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
opacity: .3;
|
||||
}
|
||||
</style>
|
||||
<radialGradient id="radial-gradient" cx="134.8386" cy="116.6086" fx="134.8386" fy="116.6086" r="40.159" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="red"/>
|
||||
<stop offset=".1498" stop-color="#f90000"/>
|
||||
<stop offset=".3381" stop-color="#ea0000"/>
|
||||
<stop offset=".547" stop-color="#d00000"/>
|
||||
<stop offset=".7707" stop-color="#ac0000"/>
|
||||
<stop offset="1" stop-color="maroon"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="linear-gradient" x1="181.204" y1="38.5062" x2="12.8241" y2="144.8752" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b3b3b3"/>
|
||||
<stop offset=".4112" stop-color="#4d4d4d"/>
|
||||
<stop offset=".5089" stop-color="#333"/>
|
||||
<stop offset="1" stop-color="#000"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-2" x1="17.6659" y1="93.9885" x2="29.999" y2="138.9568" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b3b3b3"/>
|
||||
<stop offset="1" stop-color="#333"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-3" x1="174.0229" y1="113.5193" x2="179.62" y2="82.7355" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".001" stop-color="gray"/>
|
||||
<stop offset="1" stop-color="#1a1a1a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-4" x1="247.5646" y1="110.128" x2="74.5378" y2="217.1234" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b3b3b3"/>
|
||||
<stop offset=".5042" stop-color="#4d4d4d"/>
|
||||
<stop offset=".6029" stop-color="#333"/>
|
||||
<stop offset="1" stop-color="#000"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-5" x1="249.131" y1="166.3848" x2="221.4395" y2="119.4912" xlink:href="#linear-gradient-3"/>
|
||||
<linearGradient id="linear-gradient-6" x1="92.258" y1="147.6046" x2="67.9713" y2="179.4808" xlink:href="#linear-gradient-2"/>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-7" d="M139.7025,104h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.031l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.031l-11.6667-20c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845Z"/>
|
||||
<g class="cls-9">
|
||||
<path d="M139.7025,105c1.0629,0,2.0558,.5703,2.5913,1.4884l11.6667,20c.5439,.9324,.5439,2.0908,0,3.0232l-11.6667,20c-.5356,.9181-1.5285,1.4884-2.5913,1.4884h-23.405c-1.0628,0-2.0558-.5703-2.5913-1.4884l-11.6667-20c-.5439-.9324-.5439-2.0908,0-3.0232l11.6667-20c.5355-.9181,1.5285-1.4884,2.5913-1.4884h23.405m0-1h-23.405c-1.4227,0-2.7383,.7556-3.4551,1.9845l-11.6667,20c-.7265,1.2455-.7265,2.7855,0,4.0309l11.6667,20c.7168,1.2289,2.0324,1.9845,3.4551,1.9845h23.405c1.4227,0,2.7383-.7556,3.4551-1.9845l11.6667-20c.7265-1.2455,.7265-2.7855,0-4.0309l-11.6667-20c-.7169-1.2289-2.0324-1.9845-3.4551-1.9845h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g class="cls-11">
|
||||
<path class="cls-6" d="M130.2975,128h22.2204c1.5436,0,2.5053-1.6744,1.7276-3.0077l-11.0878-19.0077c-.7168-1.2289-2.0324-1.9845-3.4551-1.9845h-22.2204c-1.5436,0-2.5053,1.6744-1.7276,3.0077l11.0878,19.0077c.7168,1.2289,2.0324,1.9845,3.4551,1.9845Z"/>
|
||||
</g>
|
||||
<g class="cls-10">
|
||||
<path d="M126.8243,130.0155l-11.0967,19.023c-.7718,1.323-2.6834,1.323-3.4551,0l-11.0967-19.023c-.7265-1.2455-.7265-2.7855,0-4.031l11.0967-19.023c.7718-1.323,2.6834-1.323,3.4551,0l11.0967,19.023c.7265,1.2455,.7265,2.7855,0,4.031Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-8" d="M208.86,84.2906l-25.551-44.2884c-1.4287-2.4765-4.0704-4.0022-6.9295-4.0022H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9768l30.3695-52.0619c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path d="M176.3795,37c2.4938,0,4.8171,1.3419,6.0633,3.502l25.551,44.2884c.4199,.7278,.5161,1.5712,.2708,2.3748-.2452,.8036-.796,1.4496-1.5507,1.8189l-42.3075,20.7037-12.5549-22.1804-.2872-.5074h-40.8206c-3.8971,0-7.5379,2.0912-9.5016,5.4575l-30.3694,52.0619c-.8926,1.5301-2.5475,2.4807-4.3189,2.4807H13.565c-1.5243,0-2.8055-1.1419-2.9802-2.6561l-5.4628-47.3439H40.5744l.2894-.4961,32.1059-55.0387c1.6066-2.7542,4.5855-4.4652,7.774-4.4652h95.6358m0-1H80.7437c-3.5567,0-6.8457,1.8891-8.6378,4.9613l-32.1059,55.0387H4l5.5914,48.4585c.2329,2.0184,1.9419,3.5415,3.9736,3.5415h52.9888c2.134,0,4.1074-1.1335,5.1827-2.9768l30.3694-52.0619c1.7921-3.0722,5.0811-4.9613,8.6378-4.9613h40.2375l13.0188,23,43.1534-21.1176c2.0806-1.0182,2.8641-3.5853,1.7065-5.5917l-25.551-44.2884c-1.4287-2.4765-4.0704-4.0022-6.9295-4.0022h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-3" d="M9.4796,143.4895l-5.4796-47.4895H40l-27.731,48.0672c-.7224,1.2521-2.6237,.8584-2.7894-.5776Z"/>
|
||||
<path class="cls-2" d="M164,111l41.1816-20.1527c1.428-.6988,.9305-2.8473-.6593-2.8473h-53.5222l13,23Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-4" d="M47.14,171.7094l25.551,44.2884c1.4287,2.4765,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9768l-30.3695,52.0619c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918Z"/>
|
||||
<g class="cls-9">
|
||||
<path d="M242.435,109c1.5243,0,2.8055,1.1419,2.9802,2.6561l5.4628,47.3439h-35.4523l-.2894,.4962-32.1059,55.0387c-1.6066,2.7542-4.5855,4.4651-7.774,4.4651H79.6205c-2.4938,0-4.8171-1.3419-6.0633-3.5019l-25.551-44.2884c-.4199-.7278-.5161-1.5713-.2708-2.3749,.2453-.8036,.796-1.4496,1.5507-1.819l42.3075-20.7036,12.5549,22.1804,.2872,.5074h40.8206c3.8971,0,7.5379-2.0912,9.5016-5.4575l30.3694-52.0619c.8926-1.5301,2.5475-2.4807,4.3189-2.4807h52.9888m0-1h-52.9888c-2.134,0-4.1074,1.1335-5.1827,2.9768l-30.3694,52.0619c-1.7921,3.0722-5.0811,4.9613-8.6378,4.9613h-40.2375l-13.0188-23-43.1534,21.1176c-2.0806,1.0182-2.8641,3.5853-1.7065,5.5918l25.551,44.2884c1.4287,2.4764,4.0704,4.0022,6.9295,4.0022h95.6358c3.5567,0,6.8457-1.8891,8.6378-4.9613l32.1059-55.0387h36l-5.5914-48.4585c-.2329-2.0184-1.9419-3.5415-3.9736-3.5415h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-5" d="M246.5204,112.5105l5.4796,47.4895h-36l27.731-48.0672c.7224-1.2521,2.6237-.8584,2.7894,.5776Z"/>
|
||||
<path class="cls-1" d="M92,145l-41.1816,20.1527c-1.428,.6988-.9305,2.8473,.6593,2.8473h53.5222l-13-23Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,33 @@
|
||||
import { css } from '@emotion/react';
|
||||
import { ThemeClasses } from '@redeye/ui-styles';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import LogoDark from './Logo-Dark.svg';
|
||||
import LogoLight from './Logo-Light.svg';
|
||||
|
||||
type LogoProps = ComponentProps<'img'> & {};
|
||||
|
||||
export const Logo: FC<LogoProps> = ({ ...props }) => (
|
||||
// switching logo based on dark theme is currently disabled
|
||||
<>
|
||||
<img
|
||||
{...props}
|
||||
src={LogoDark}
|
||||
alt="RedEye Logo"
|
||||
css={css`
|
||||
.${ThemeClasses.LIGHT} & {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<img
|
||||
{...props}
|
||||
src={LogoLight}
|
||||
alt="RedEye Logo"
|
||||
css={css`
|
||||
.${ThemeClasses.DARK} & {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
import { css } from '@emotion/react';
|
||||
import { useStore } from '@redeye/client/store';
|
||||
import type { PopoverButtonProps } from '@redeye/ui-styles';
|
||||
import { Flex, ExternalLink, CoreTokens, PopoverButton, Txt } from '@redeye/ui-styles';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
export const ModeIndicator = observer<Omit<PopoverButtonProps, 'content'> & { fullName?: boolean }>(
|
||||
({ popoverProps, fullName, ...props }) => {
|
||||
const store = useStore();
|
||||
const isRedTeam = !store.appMeta.blueTeam;
|
||||
|
||||
let teamAcronym = fullName ? 'Blue Team' : 'BT';
|
||||
let team = 'Blue Team';
|
||||
let description = 'Restricted RedEye functionality. Editing and commenting features are not available.';
|
||||
let teamCSS = blueStyle;
|
||||
|
||||
if (isRedTeam) {
|
||||
teamAcronym = fullName ? 'Red Team' : 'RT';
|
||||
team = 'Red Team';
|
||||
description = 'Full RedEye functionality. Editing and commenting is not restricted.';
|
||||
teamCSS = redStyle;
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverButton
|
||||
popoverProps={{
|
||||
position: 'right',
|
||||
interactionKind: 'hover',
|
||||
...popoverProps,
|
||||
}}
|
||||
content={
|
||||
<Flex column gap={4} align="start" css={{ width: 320, padding: 12 }}>
|
||||
<Txt large bold>
|
||||
<Txt>{team}</Txt> <Txt muted>Mode</Txt>
|
||||
</Txt>
|
||||
<Txt muted>{description}</Txt>
|
||||
<ExternalLink href="https://github.com/cisagov/redeye#red-team--blue-team-modes">Learn more</ExternalLink>
|
||||
</Flex>
|
||||
}
|
||||
active={false}
|
||||
{...props}
|
||||
>
|
||||
<div cy-test={teamAcronym} css={teamCSS}>
|
||||
{teamAcronym}
|
||||
</div>
|
||||
</PopoverButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const indicatorStyle = css`
|
||||
padding: 2px;
|
||||
font-weight: 800;
|
||||
font-size: 11px;
|
||||
line-height: 11px;
|
||||
text-transform: uppercase;
|
||||
color: ${CoreTokens.Colors.Black};
|
||||
`;
|
||||
|
||||
const blueStyle = css`
|
||||
${indicatorStyle};
|
||||
background-color: ${CoreTokens.Colors.Blue3};
|
||||
`;
|
||||
|
||||
const redStyle = css`
|
||||
${indicatorStyle};
|
||||
background-color: ${CoreTokens.Colors.Red3};
|
||||
`;
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { ButtonProps } from '@blueprintjs/core';
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { PresentationFile24, Search24, Terminal24 } from '@carbon/icons-react';
|
||||
import { css } from '@emotion/react';
|
||||
import { AppOptions, CarbonIcon, Logo } from '@redeye/client/components';
|
||||
import { routes, useStore } from '@redeye/client/store';
|
||||
import { Tabs } from '@redeye/client/types/explore';
|
||||
import { CoreTokens, FlexSplitter } from '@redeye/ui-styles';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import { CampaignViews, Views } from '../../types';
|
||||
import { SearchPanelOverlay } from '../../views/Campaign/Search/SearchPanelOverlay';
|
||||
|
||||
type NavBarProps = ComponentProps<'header'> & {};
|
||||
|
||||
export const NavBar = observer<NavBarProps>(({ ...props }) => {
|
||||
const store = useStore(); // TODO: buttons navigate to new routes
|
||||
|
||||
return (
|
||||
<header {...props} css={wrapperStyle}>
|
||||
<Logo
|
||||
cy-test="return-campaign-menu"
|
||||
css={logoStyle}
|
||||
title="Return to Campaign Menu"
|
||||
onClick={() =>
|
||||
store.router.updateRoute({
|
||||
path: routes[Views.CAMPAIGNS_LIST],
|
||||
params: { id: store.router.params.id || 'all' },
|
||||
clear: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavButton
|
||||
cy-test="explorer-mode"
|
||||
icon={<CarbonIcon icon={Terminal24} />}
|
||||
title="Explorer Mode"
|
||||
active={store.router.params.view === CampaignViews.EXPLORE}
|
||||
onClick={() =>
|
||||
store.router.updateRoute({
|
||||
path: routes[CampaignViews.EXPLORE],
|
||||
params: {
|
||||
id: store.campaign.id,
|
||||
view: CampaignViews.EXPLORE,
|
||||
tab: Tabs.HOSTS,
|
||||
currentItem: 'all',
|
||||
currentItemId: undefined,
|
||||
activeItem: undefined,
|
||||
activeItemId: undefined,
|
||||
},
|
||||
queryParams: store.router.queryParams.search
|
||||
? {
|
||||
search: store.router.queryParams.search,
|
||||
'search-modal': store.router.queryParams['search-modal'],
|
||||
}
|
||||
: {},
|
||||
clear: store.router.params.tab !== 'beacons' && store.router.params.currentItem !== 'all',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavButton
|
||||
cy-test="presentation-mode"
|
||||
icon={<CarbonIcon icon={PresentationFile24} />}
|
||||
title="Presentation Mode"
|
||||
active={store.router.params.view === CampaignViews.PRESENTATION}
|
||||
onClick={() =>
|
||||
store.router.updateRoute({
|
||||
path: routes[CampaignViews.PRESENTATION],
|
||||
params: {
|
||||
id: store.campaign.id,
|
||||
view: CampaignViews.PRESENTATION,
|
||||
},
|
||||
clear: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavButton
|
||||
cy-test="search-mode"
|
||||
icon={<CarbonIcon icon={Search24} />}
|
||||
title="Search"
|
||||
active={store.campaign.search.isSearchModalOpen}
|
||||
onClick={() => {
|
||||
store.campaign.search.openSearch();
|
||||
}}
|
||||
/>
|
||||
<FlexSplitter />
|
||||
<AppOptions navBar />
|
||||
<SearchPanelOverlay
|
||||
isOpen={store.campaign.search.isSearchModalOpen}
|
||||
onClose={() => {
|
||||
store.campaign.search.closeSearch();
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
||||
const wrapperStyle = css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 4rem;
|
||||
padding-bottom: 1rem;
|
||||
background-color: ${CoreTokens.Background1};
|
||||
`;
|
||||
const logoSize = 48;
|
||||
const logoStyle = css`
|
||||
height: ${logoSize}px;
|
||||
width: ${logoSize}px;
|
||||
margin: 1rem 2px;
|
||||
cursor: pointer;
|
||||
transition: 100ms ease-in transform;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
`;
|
||||
|
||||
type NavButtonProps = ButtonProps & { title?: string };
|
||||
|
||||
export const NavButton: FC<NavButtonProps> = ({ active, ...props }) => (
|
||||
<Button {...props} css={[navButtonStyles, active ? navButtonActiveStyles : undefined]} minimal fill />
|
||||
);
|
||||
|
||||
const navButtonStyles = css`
|
||||
border: solid transparent;
|
||||
border-width: 0 3px;
|
||||
margin: 1px 0;
|
||||
height: 3rem;
|
||||
`;
|
||||
|
||||
const navButtonActiveStyles = css`
|
||||
border-left-color: ${CoreTokens.Intent.Primary3};
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from './AppOptions';
|
||||
export * from './AppTitle';
|
||||
export * from './Header';
|
||||
export * from './Logo';
|
||||
export * from './NavBar';
|
||||
export * from './ModeIndicator';
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ButtonProps } from '@blueprintjs/core';
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import type { FC } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = ButtonProps & {
|
||||
hoverProps?: ButtonProps;
|
||||
};
|
||||
|
||||
/** <Button/> that can change its props when hovered */
|
||||
export const HoverButton: FC<Props> = ({ hoverProps = {}, ...props }) => {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const mergedProps = {
|
||||
...props, // initial props
|
||||
...(isHovering ? hoverProps : {}), // overridden with hover props
|
||||
};
|
||||
return <Button onMouseOver={() => setIsHovering(true)} onMouseOut={() => setIsHovering(false)} {...mergedProps} />;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Txt } from '@redeye/ui-styles';
|
||||
import { mitreAttackDictionary } from './mitreAttackDictionary';
|
||||
|
||||
type MitreAttackItem = {
|
||||
name: string;
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
export type MitreAttackId = keyof typeof mitreAttackDictionary;
|
||||
// type MitreAttackDictionary = Record<keyof typeof mitreAttackDictionary, MitreAttackItem>;
|
||||
|
||||
type MitreAttackProps = ComponentProps<'a'> & {
|
||||
miterAttackId: keyof typeof mitreAttackDictionary;
|
||||
};
|
||||
|
||||
export const MitreAttack = observer<MitreAttackProps>(({ miterAttackId, ...props }) => {
|
||||
const mitreAttackItem = mitreAttackDictionary[miterAttackId] as MitreAttackItem | undefined;
|
||||
const { name, id, url } = mitreAttackItem || {};
|
||||
if (mitreAttackItem)
|
||||
return (
|
||||
<a
|
||||
children={`${id}: ${name}`}
|
||||
aria-label="Mitre attack links"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
else return <Txt muted>{miterAttackId}</Txt>;
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './MitreAttack';
|
||||
export * from './mitreAttackDictionary';
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Run this to generate a dataset of named MITRE ATT&CKs for use in the UI
|
||||
* `node ./process-enterprise-attack.js`
|
||||
* https://attack.mitre.org/
|
||||
* https://github.com/mitre/cti
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// manually update this file from https://github.com/mitre/cti/blob/master/enterprise-attack/enterprise-attack.json
|
||||
const enterpriseAttack = require('./enterprise-attack.json');
|
||||
// we could also add the other items from ics, mobile, and pre? if they have the same format
|
||||
|
||||
const mitreAttackDictionary = {};
|
||||
|
||||
enterpriseAttack.objects.forEach((object) => {
|
||||
const externalReference = object.external_references?.find((ref) => ref.source_name === 'mitre-attack');
|
||||
|
||||
// some of the externalReferences don't have a mitre-attack associated
|
||||
if (!externalReference) return { name: object.name };
|
||||
|
||||
const { external_id: id, url } = externalReference;
|
||||
|
||||
const mitreAttack = {
|
||||
name: object.name,
|
||||
id,
|
||||
url,
|
||||
};
|
||||
|
||||
// Mire Attacks can have sub attacks formatted 'T0000.000'
|
||||
const [parentTechnique, subTechnique] = id.split('.');
|
||||
|
||||
if (subTechnique != null) {
|
||||
mitreAttack.parentTechnique = parentTechnique;
|
||||
|
||||
// add subTechnique to parentTechnique
|
||||
if (mitreAttackDictionary[parentTechnique] == null) {
|
||||
mitreAttackDictionary[parentTechnique] = { subTechniques: [subTechnique] };
|
||||
} else if (mitreAttackDictionary[parentTechnique].subTechniques == null) {
|
||||
mitreAttackDictionary[parentTechnique].subTechniques = [subTechnique];
|
||||
} else {
|
||||
mitreAttackDictionary[parentTechnique].subTechniques.push(subTechnique);
|
||||
}
|
||||
}
|
||||
|
||||
// mitreAttackDictionary[id] may have been added from the subTechnique process
|
||||
if (mitreAttackDictionary[id] == null) {
|
||||
mitreAttackDictionary[id] = mitreAttack;
|
||||
} else {
|
||||
mitreAttackDictionary[id] = {
|
||||
...mitreAttackDictionary[id],
|
||||
...mitreAttack,
|
||||
};
|
||||
}
|
||||
|
||||
return mitreAttack;
|
||||
});
|
||||
|
||||
const alphabeticalMitreAttackDictionary = {};
|
||||
Object.keys(mitreAttackDictionary)
|
||||
.sort()
|
||||
.forEach((id) => {
|
||||
alphabeticalMitreAttackDictionary[id] = mitreAttackDictionary[id].subTechniques
|
||||
? {
|
||||
...mitreAttackDictionary[id],
|
||||
subTechniques: mitreAttackDictionary[id].subTechniques.sort(),
|
||||
}
|
||||
: mitreAttackDictionary[id];
|
||||
});
|
||||
|
||||
globalThis.console.log(`Parsed ${Object.entries(alphabeticalMitreAttackDictionary).length} MITRE ATT&CK ids`);
|
||||
|
||||
// it helps to manually run prettier on this after its generated
|
||||
const mitreAttackDictionaryPathTs = path.join(__dirname, 'mitreAttackDictionary.ts');
|
||||
const tsFileContents = `export const mitreAttackDictionary = ${JSON.stringify(alphabeticalMitreAttackDictionary)}`;
|
||||
fs.writeFile(mitreAttackDictionaryPathTs, tsFileContents, (err) => {
|
||||
if (err) globalThis.console.error(err);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
# Download MITRE ATT&CK Framework Json
|
||||
|
||||
https://github.com/mitre/cti/blob/master/enterprise-attack/enterprise-attack.json
|
||||
|
||||
This data may need to be updated periodically
|
||||
|
||||
- Visit https://mitre-attack.github.io/attack-navigator/
|
||||
- Select "Create New Layer" > "Enterprise"
|
||||
- In the top toolbar under "Selection Controls" click "Search & MultiSelect" (the search icon 🔎)
|
||||
- In the right panel, under "Techniques" click "Select All."
|
||||
- With all techniques selected, In the top toolbar under "Layer Controls" click "Download layer as json" (the down arrow icon ⬇)
|
||||
- replace the `layer.json` file with the newly downloaded one.
|
||||
@@ -0,0 +1,22 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { UtilityStyles } from '@redeye/ui-styles';
|
||||
|
||||
export const scrollBoxOuterStyle = css`
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
${UtilityStyles.innerBoxShadowOverlay('vertical')};
|
||||
`;
|
||||
export const ScrollBox = styled.div(scrollBoxOuterStyle);
|
||||
|
||||
export const scrollChildStyle = css`
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
export const ScrollChild = styled.div(scrollChildStyle);
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ErrorFallback } from '@redeye/client/components';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import type { ListProps, VirtuosoProps } from 'react-virtuoso';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { useStore } from '../store';
|
||||
import { createState } from './mobx-create-state';
|
||||
|
||||
type VirtualizedListProps = VirtuosoProps<ListProps, any> & {
|
||||
paddingBottom?: number;
|
||||
listRef?: any;
|
||||
initialIndex?: number;
|
||||
};
|
||||
|
||||
export const VirtualizedList = observer<VirtualizedListProps>(
|
||||
({ children, paddingBottom = 200, listRef, initialIndex, itemsRendered, ...props }) => {
|
||||
const store = useStore();
|
||||
const flatChildren = Array.isArray(children) ? children?.flat() : children;
|
||||
const state = createState({
|
||||
clearGraphHover() {
|
||||
setTimeout(() => {
|
||||
store.campaign?.interactionState.onHoverOut({
|
||||
beacon: undefined,
|
||||
host: undefined,
|
||||
server: undefined,
|
||||
});
|
||||
}, 75);
|
||||
},
|
||||
updateScrollIndex(items) {
|
||||
store.router.replaceState({
|
||||
scrollKey: items[0]?.index,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return Array.isArray(flatChildren) ? (
|
||||
<Virtuoso
|
||||
onMouseLeave={state.clearGraphHover}
|
||||
initialTopMostItemIndex={initialIndex ?? (store.router.location.state?.scrollKey || 0)}
|
||||
itemsRendered={(items) => {
|
||||
if (itemsRendered) {
|
||||
itemsRendered(items);
|
||||
} else {
|
||||
state.updateScrollIndex(items);
|
||||
}
|
||||
}}
|
||||
itemContent={(index) => flatChildren[index]}
|
||||
totalCount={flatChildren.length}
|
||||
css={{ height: '100%' }}
|
||||
ref={listRef}
|
||||
overscan={100} // in px
|
||||
components={{ Footer: () => <div css={{ height: paddingBottom }} /> }}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<div>{children}</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { SortOption } from '../store';
|
||||
|
||||
/** Creates a generic sorter for a specific property and for ascending and descending */
|
||||
export function createSorter<T>(
|
||||
sort?: SortOption | null | undefined | string | ((arg0: T) => any),
|
||||
isAscending: boolean = true
|
||||
): (a: T, b: T) => number {
|
||||
if (!sort) return () => 0;
|
||||
return (aFull, bFull) => {
|
||||
// Select the values
|
||||
let a;
|
||||
let b;
|
||||
if (typeof sort === 'function') {
|
||||
a = sort(aFull);
|
||||
b = sort(bFull);
|
||||
} else {
|
||||
a = aFull[sort!];
|
||||
b = bFull[sort!];
|
||||
}
|
||||
|
||||
// Check for undefined things (use == null so we catch undefined/null values)
|
||||
if (a == null || b == null) {
|
||||
if (a || b) {
|
||||
// either a or b is undefined (exclusively)
|
||||
if (!a) {
|
||||
return 1; // b should appear above a
|
||||
} else {
|
||||
return -1; // a should appear above b
|
||||
}
|
||||
} else {
|
||||
// both are undefined
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// case insensitive for strings
|
||||
if (typeof a === 'string' && typeof b === 'string') {
|
||||
a = a.toUpperCase();
|
||||
b = b.toUpperCase();
|
||||
}
|
||||
|
||||
let returnVal = 0; // initially assume a === b
|
||||
if (a < b) {
|
||||
// a is less than b by some ordering criterion
|
||||
returnVal = -1;
|
||||
} else if (a > b) {
|
||||
// a is greater than b by the ordering criterion
|
||||
returnVal = 1;
|
||||
}
|
||||
|
||||
// Reverse-ish ordering if descending
|
||||
if (!isAscending) {
|
||||
returnVal = -returnVal;
|
||||
}
|
||||
return returnVal;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { UtilityStyles } from '@redeye/ui-styles';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
// Sanitize input for use in regex
|
||||
function sanitizeRegex(text: string): string {
|
||||
return (text ?? '').replace(/[#-.]|[[-^]|[?|{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
function tokenize(str) {
|
||||
return str.split(/\s/gi);
|
||||
}
|
||||
|
||||
function createRegex(pattern: string) {
|
||||
return `(?:${tokenize(sanitizeRegex(pattern)).join('|')})`;
|
||||
}
|
||||
|
||||
export const highlightPattern = (text?: string, pattern?: string): ReactNode => {
|
||||
if (!text) return null;
|
||||
if (!pattern) return text;
|
||||
|
||||
const regEx = new RegExp(createRegex(pattern), 'ig');
|
||||
const splitText = text?.split?.(regEx) || '';
|
||||
if (splitText.length <= 1) return text;
|
||||
|
||||
const matches = text.match(regEx);
|
||||
if (!matches) return text;
|
||||
|
||||
return splitText.reduce<ReactNode[]>(
|
||||
(arr, element, index) =>
|
||||
matches[index]
|
||||
? [
|
||||
...arr,
|
||||
element,
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<span key={`${element}-${index}`} css={UtilityStyles.textHighlight}>
|
||||
{matches[index]}
|
||||
</span>,
|
||||
]
|
||||
: [...arr, element],
|
||||
[]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from './AuthCheck';
|
||||
export * from './CarbonIcon';
|
||||
export * from './DragResize';
|
||||
export * from './Dropdown';
|
||||
export * from './ErrorFallback';
|
||||
export * from './ExpandingSearchBox';
|
||||
export * from './HoverButton';
|
||||
export * from './ScrollBox';
|
||||
export * from './VirtualizedList';
|
||||
export * from './create-sorter';
|
||||
export * from './is-defined-guard';
|
||||
export * from './highlightPattern';
|
||||
export * from './mobx-create-state';
|
||||
export * from './utils-time';
|
||||
export * from './utils';
|
||||
export * from './Dialogs/index';
|
||||
export * from './Forms/index';
|
||||
export * from './Header/index';
|
||||
export * from './Mitre/index';
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isDefined<T>(argument: T | undefined | unknown): argument is T {
|
||||
return argument !== undefined;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { AnnotationsMap } from 'mobx';
|
||||
import { runInAction } from 'mobx';
|
||||
import { useLocalObservable as mbxLocalObservable } from 'mobx-react-lite';
|
||||
|
||||
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
||||
|
||||
type ReadonlyKeys<T> = {
|
||||
[P in keyof T]-?: IfEquals<
|
||||
{
|
||||
[Q in P]: T[P];
|
||||
},
|
||||
{
|
||||
-readonly [Q in P]: T[P];
|
||||
},
|
||||
never,
|
||||
P
|
||||
>;
|
||||
}[keyof T];
|
||||
|
||||
export type UseCreateState<T, ReadOnly extends ReadonlyKeys<T> = ReadonlyKeys<T>> = Pick<T, ReadOnly> &
|
||||
Immutable<Omit<T, ReadOnly>> & {
|
||||
update: <K extends keyof T>(...input: [K, T[K]] | [(update: T) => void]) => void;
|
||||
};
|
||||
|
||||
export type Immutable<Obj> = {
|
||||
+readonly [K in keyof Obj]: Obj[K];
|
||||
};
|
||||
|
||||
export const createState = <T extends object>(init: T, annotations?: AnnotationsMap<T, never>): UseCreateState<T> => {
|
||||
const state = mbxLocalObservable(() => {
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const key in init) {
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(init, key);
|
||||
if (!descriptor?.get && typeof init[key] === 'function') {
|
||||
// @ts-ignore
|
||||
init[key].bind(init);
|
||||
}
|
||||
}
|
||||
return Object.assign(init, {
|
||||
update(name, value) {
|
||||
runInAction(() => {
|
||||
if (typeof value === 'function') value(this);
|
||||
else this[name as any] = value;
|
||||
});
|
||||
},
|
||||
});
|
||||
}, annotations);
|
||||
|
||||
return state as unknown as UseCreateState<T>;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { timeDay, timeYear } from 'd3';
|
||||
|
||||
export enum TickFormat {
|
||||
Second = 'Second',
|
||||
Minute = 'Minute',
|
||||
Hour = 'Hour',
|
||||
Day = 'Day',
|
||||
Month = 'Month',
|
||||
YearMonth = 'YearMonth',
|
||||
Year = 'Year',
|
||||
}
|
||||
|
||||
export const formatters: {
|
||||
[key in TickFormat]: string | ((arg: Date) => string);
|
||||
} = {
|
||||
[TickFormat.Second]: 'HH:mm:ss',
|
||||
[TickFormat.Minute]: 'HH:mm',
|
||||
[TickFormat.Hour]: (date) => {
|
||||
// if midnight show MM/DD HH:mm
|
||||
const rounded = timeDay(date);
|
||||
if (rounded.valueOf() === date.valueOf()) {
|
||||
return 'MM/DD';
|
||||
} else {
|
||||
return 'HH:mm';
|
||||
}
|
||||
},
|
||||
[TickFormat.Day]: 'MM/DD',
|
||||
[TickFormat.Month]: 'MMM',
|
||||
[TickFormat.YearMonth]: (date) => {
|
||||
// if Jan, do YYYY or MMM YYYY
|
||||
const rounded = timeYear(date);
|
||||
if (rounded.valueOf() === date.valueOf()) {
|
||||
return 'YYYY';
|
||||
} else {
|
||||
return 'MMM';
|
||||
}
|
||||
},
|
||||
[TickFormat.Year]: 'YYYY',
|
||||
};
|
||||
|
||||
const durationSecond = 1;
|
||||
const durationMinute = durationSecond * 60;
|
||||
const durationHour = durationMinute * 60;
|
||||
const durationDay = durationHour * 24;
|
||||
const durationWeek = durationDay * 7;
|
||||
const durationMonth = durationDay * 30;
|
||||
const durationYear = durationDay * 365;
|
||||
|
||||
export const tickFormatIntervals: [TickFormat, number][] = [
|
||||
[TickFormat.Second, 30 * durationSecond], // use TickFormats.Second for interval x <= 30 seconds
|
||||
[TickFormat.Minute, durationHour],
|
||||
[TickFormat.Hour, 12 * durationHour],
|
||||
[TickFormat.Day, durationWeek],
|
||||
[TickFormat.Month, durationMonth],
|
||||
[TickFormat.YearMonth, durationYear],
|
||||
[TickFormat.Year, 2 * durationYear],
|
||||
];
|
||||
|
||||
export const dateFormat = 'MM/DD/YY';
|
||||
export const datePlaceholder = '00/00/00';
|
||||
export const dateShortFormat = 'MM/DD';
|
||||
export const dateShortPlaceholder = '00/00';
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timePlaceholder = '00:00';
|
||||
export const dateTimeFormat = `${dateFormat} ${timeFormat}`;
|
||||
export const dateTimePlaceholder = `${datePlaceholder} ${timePlaceholder}`;
|
||||
|
||||
export const durationFormatter = (date1?: Date | null, date2?: Date | null) => {
|
||||
if (!date1 || !date2) return undefined;
|
||||
const duration = Math.abs(date1.getTime() - date2.getTime()) / 1000;
|
||||
return duration > durationMonth ? dateFormat : dateTimeFormat;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export const updatePopper = document.createEvent('Event');
|
||||
updatePopper.initEvent('resize', true, false);
|
||||
// this forces Popper.js to update any instances by triggering a native window resize event
|
||||
// document.dispatchEvent(updatePopper);
|
||||
|
||||
/**
|
||||
* Escapes arbitrary text to get it ready for regex. (like for user search text input)
|
||||
* @param text
|
||||
*/
|
||||
export function escapeRegExpChars(text: string) {
|
||||
return text.replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1');
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
interface ImportMetaEnv {
|
||||
VITE_BLUE_TEAM: string;
|
||||
VITE_SERVER_URL: string;
|
||||
}
|
||||
|
||||
declare const PACKAGE_VERSION: string;
|
||||
|
||||
declare module '*?worker' {
|
||||
const workerConstructor: {
|
||||
new (): Worker;
|
||||
};
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var,vars-on-top
|
||||
var Cypress: any | undefined;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Global } from '@emotion/react';
|
||||
import { customCssVars, globalStyle } from '@redeye/ui-styles';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import { CustomRouter } from './CustomRouter';
|
||||
import { AppStoreProvider, store, updateAppTheme } from './store';
|
||||
|
||||
// necessary for blueprint to function
|
||||
import 'blueprint-styler/base/blueprint-tokens.css';
|
||||
import 'blueprint-styler/base/blueprint.css';
|
||||
import 'blueprint-styler/overrides/carbon/override-tokens.css';
|
||||
import 'blueprint-styler/overrides/carbon/override.css';
|
||||
|
||||
updateAppTheme();
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Global styles={[customCssVars, globalStyle]} />
|
||||
<AppStoreProvider store={store}>
|
||||
<QueryClientProvider client={store.queryClient}>
|
||||
<CustomRouter history={store.router.history}>
|
||||
<App />
|
||||
</CustomRouter>
|
||||
</QueryClientProvider>
|
||||
</AppStoreProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { AppStore } from './app-store';
|
||||
|
||||
const StoreContext = createContext<null | AppStore>(null);
|
||||
|
||||
export function useStore(): AppStore {
|
||||
const rootStore = useContext(StoreContext);
|
||||
if (rootStore === null) {
|
||||
throw new Error('store cannot be null, please add a context provider');
|
||||
}
|
||||
return rootStore;
|
||||
}
|
||||
|
||||
export const AppStoreProvider = observer((props: PropsWithChildren<{ store: AppStore }>) => (
|
||||
<StoreContext.Provider value={props.store}>{props.children}</StoreContext.Provider>
|
||||
));
|
||||
@@ -0,0 +1,114 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { createHttpClient } from 'mk-gql';
|
||||
import { reaction } from 'mobx';
|
||||
import type { ObjectMap } from 'mobx-keystone';
|
||||
import { detach, model, Model, modelAction, prop, registerRootStore } from 'mobx-keystone';
|
||||
import { Auth } from './auth';
|
||||
import { CampaignStore } from './campaign';
|
||||
import type {
|
||||
BeaconModel,
|
||||
CommandModel,
|
||||
CommandTypeCountModel,
|
||||
HostModel,
|
||||
OperatorModel,
|
||||
ServerModel,
|
||||
} from './graphql';
|
||||
import { appRef, RootStore } from './graphql';
|
||||
import { Router } from './routing/router';
|
||||
import { Settings } from './settings';
|
||||
import { campaignsQuery } from './util/cleaned-queries';
|
||||
|
||||
@model('AppStore')
|
||||
export class AppStore extends Model({
|
||||
graphqlStore: prop<RootStore>(() => new RootStore({ enableCache: false })),
|
||||
campaign: prop<CampaignStore>(() => new CampaignStore({})),
|
||||
auth: prop<Auth>(() => new Auth({})),
|
||||
router: prop<Router>(() => new Router({})),
|
||||
settings: prop<Settings>(() => new Settings({})),
|
||||
appMeta: prop<{ blueTeam: boolean }>(() => ({ blueTeam: false })).withSetter(),
|
||||
}) {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
protected onAttachedToRootStore() {
|
||||
// Setup graphql store
|
||||
this.graphqlStore.middleWare = (resp: Promise<Response>) => {
|
||||
resp.catch((e) => {
|
||||
if (e.message.startsWith('Access denied! You need to be authorized to perform this action!'))
|
||||
this.auth.logOut();
|
||||
return e;
|
||||
});
|
||||
};
|
||||
|
||||
fetch(`${this.auth.serverUrl}/api/appMetadata`, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
credentials: 'include',
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((value) => {
|
||||
this.setAppMeta(value);
|
||||
});
|
||||
|
||||
this.graphqlStore.gqlHttpClient = createHttpClient(`${this.auth.serverUrl}/api/graphql`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
// Get initial list of campaigns
|
||||
this.graphqlStore.queryCampaigns({}, campaignsQuery);
|
||||
|
||||
return reaction(
|
||||
() => this.router.params.id,
|
||||
() => {
|
||||
this.reset();
|
||||
this.queryClient.getQueryCache().clear();
|
||||
this.queryClient.invalidateQueries();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@modelAction reset(withCampaign: boolean = true) {
|
||||
const detachAndClear = (item: ObjectMap<any>) => {
|
||||
item.clear();
|
||||
};
|
||||
|
||||
if (withCampaign) {
|
||||
detach(this.campaign);
|
||||
this.campaign = new CampaignStore({ isLoading: '' });
|
||||
}
|
||||
detachAndClear(this.graphqlStore?.commands);
|
||||
detachAndClear(this.graphqlStore?.beacons);
|
||||
|
||||
detachAndClear(this.graphqlStore?.servers);
|
||||
detachAndClear(this.graphqlStore?.links);
|
||||
detachAndClear(this.graphqlStore?.hosts);
|
||||
|
||||
detachAndClear(this.graphqlStore?.commandGroups);
|
||||
detachAndClear(this.graphqlStore?.commandTypeCounts);
|
||||
detachAndClear(this.graphqlStore?.logEntries);
|
||||
detachAndClear(this.graphqlStore?.annotations);
|
||||
detachAndClear(this.graphqlStore?.tags);
|
||||
detachAndClear(this.graphqlStore?.operators);
|
||||
}
|
||||
}
|
||||
|
||||
export const store = new AppStore({});
|
||||
registerRootStore(store);
|
||||
|
||||
export const beaconCampaignRef = appRef<BeaconModel>(AppStore, 'Beacon', 'graphqlStore.beacons');
|
||||
export const hostCampaignRef = appRef<HostModel>(AppStore, 'Host', 'graphqlStore.hosts');
|
||||
export const serverCampaignRef = appRef<ServerModel>(AppStore, 'Server', 'graphqlStore.servers');
|
||||
export const operatorCampaignRef = appRef<OperatorModel>(AppStore, 'Operator', 'graphqlStore.operators');
|
||||
export const commandTypeCampaignRef = appRef<CommandTypeCountModel>(
|
||||
AppStore,
|
||||
'CommandTypeCount',
|
||||
'graphqlStore.commandTypeCounts'
|
||||
);
|
||||
|
||||
export const commandCampaignRef = appRef<CommandModel>(AppStore, 'Command', 'graphqlStore.command');
|
||||