├── .editorconfig
├── .erb
├── configs
│ ├── .eslintrc
│ ├── webpack.config.base.js
│ ├── webpack.config.eslint.js
│ ├── webpack.config.main.prod.babel.js
│ ├── webpack.config.renderer.dev.babel.js
│ ├── webpack.config.renderer.dev.dll.babel.js
│ └── webpack.config.renderer.prod.babel.js
├── img
│ ├── erb-banner.png
│ ├── erb-logo.png
│ ├── eslint-padded-90.png
│ ├── eslint-padded.png
│ ├── eslint.png
│ ├── jest-padded-90.png
│ ├── jest-padded.png
│ ├── jest.png
│ ├── js-padded.png
│ ├── js.png
│ ├── npm.png
│ ├── react-padded-90.png
│ ├── react-padded.png
│ ├── react-router-padded-90.png
│ ├── react-router-padded.png
│ ├── react-router.png
│ ├── react.png
│ ├── webpack-padded-90.png
│ ├── webpack-padded.png
│ ├── webpack.png
│ ├── yarn-padded-90.png
│ ├── yarn-padded.png
│ └── yarn.png
├── mocks
│ └── fileMock.js
└── scripts
│ ├── .eslintrc
│ ├── BabelRegister.js
│ ├── CheckBuildsExist.js
│ ├── CheckNativeDep.js
│ ├── CheckNodeEnv.js
│ ├── CheckPortInUse.js
│ ├── DeleteSourceMaps.js
│ ├── ElectronRebuild.js
│ └── Notarize.js
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── PUBLISH.md
├── README.md
├── SECURITY.md
├── assets
├── assets.d.ts
├── back-arrow.svg
├── entitlements.mac.plist
├── green-tick.svg
├── house.svg
├── icon.icns
├── icon.ico
├── icon.png
├── icon.svg
├── icons
│ ├── 1024x1024.png
│ ├── 128x128.png
│ ├── 16x16.png
│ ├── 24x24.png
│ ├── 256x256.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── 512x512.png
│ ├── 64x64.png
│ └── 96x96.png
├── no-icon-app.png
├── offline.svg
├── pro-plan-promo.svg
├── profile.svg
├── right-arrow.svg
├── suisseintl-regular.woff
├── suisseintlmono-regular.woff
├── sync.svg
├── tick.svg
├── upgrade-icon.svg
└── vulns.svg
├── babel.config.js
├── package.json
├── resources
└── osqueryi
├── rootPath.js
├── src
├── App.global.css
├── App.tsx
├── __tests__
│ └── App.test.tsx
├── app
│ ├── osqueryRefreshThunk.js
│ ├── persistAppsVulnsMiddleware.js
│ ├── persistSubscriptionThunk.js
│ ├── repoThunk.js
│ ├── store.js
│ └── testStore.js
├── components
│ ├── ActiveHardeningItem.tsx
│ ├── DashboardApp.tsx
│ ├── DashboardAppPromo.tsx
│ ├── DashboardOnboarding.tsx
│ ├── FreePlan.tsx
│ ├── InactiveHardeningItem.tsx
│ ├── NoVulnsSummary.tsx
│ ├── PaidPlan.tsx
│ ├── VulnerabilityInfo.tsx
│ ├── VulnerableAppShortInfo.tsx
│ └── VulnsSummary.tsx
├── configs.js
├── containers
│ ├── AppsWidget.tsx
│ ├── CodeActivation.tsx
│ ├── Dashboard.tsx
│ ├── DashboardTitle.tsx
│ ├── Debug.tsx
│ ├── Hardening.tsx
│ ├── SidebarIcon.tsx
│ ├── Subscription.tsx
│ ├── SyncStatus.tsx
│ ├── TrialActivation.tsx
│ ├── VulnerableAppFullInfo.tsx
│ ├── VulnerableAppFullInfoContainer.tsx
│ ├── VulnerableAppShortInfoContainer.tsx
│ └── VulnerableAppsList.tsx
├── features
│ ├── analyticsSlice
│ │ ├── analyticsSlice.js
│ │ └── utils
│ │ │ ├── recalculateAnalytics.js
│ │ │ ├── refreshStats.js
│ │ │ ├── refreshStats.measurePatchVelocity.spec.js
│ │ │ └── refreshStats.spec.js
│ ├── appsVulnerabilities
│ │ ├── appVersionsAffectedBy.js
│ │ ├── appsVulnsSlice.js
│ │ ├── coldRepoExpired.js
│ │ ├── convertToCPEName.js
│ │ ├── cutAppExtension.js
│ │ ├── detectAffectedApps.js
│ │ ├── dumbVulnRepository.js
│ │ ├── filterAffectedHostApps.js
│ │ ├── findVulnsFor.js
│ │ ├── getAffectedHostApps.js
│ │ ├── isRelevantVuln.js
│ │ ├── makeAppRepoAliases.js
│ │ ├── makeAppRepoNamesSet.js
│ │ ├── mixinVulns.js
│ │ ├── nowUnixTime.js
│ │ ├── service
│ │ │ └── vulnApi.js
│ │ ├── tests
│ │ │ ├── convertToCPEName.spec.js
│ │ │ ├── forcedVuln.spec.js
│ │ │ ├── makeAppRepoAliases.spec.js
│ │ │ ├── makeAppRepoNamesSet.spec.js
│ │ │ ├── osqueryApp.spec.js
│ │ │ └── versionBelongs.spec.js
│ │ ├── types
│ │ │ ├── osqueryApp.tsx
│ │ │ ├── osqueryOS.tsx
│ │ │ └── osqueryProduct.tsx
│ │ └── versionBelongs.js
│ ├── statusSlice
│ │ └── statusSlice.js
│ └── subscriptionSlice
│ │ └── subscriptionSlice.js
├── index.html
├── index.tsx
├── loading.html
├── main.dev.ts
├── main.prod.js.LICENSE.txt
├── menu.ts
├── package.json
├── styles
│ ├── base.css
│ ├── dashboard.css
│ └── fonts.css
├── utils
│ ├── nvd
│ │ ├── filterRelevantCVENames.js
│ │ ├── filterRelevantCVENames.spec.js
│ │ ├── getCPEName.js
│ │ └── getCPEName.spec.js
│ ├── osquery
│ │ ├── filterByNotMatchingName.js
│ │ ├── osqueryi.js
│ │ ├── osqueryiAsync.ts
│ │ └── tests
│ │ │ └── filterByNotMatchingName.spec.js
│ ├── persist
│ │ ├── analyticsAsync.ts
│ │ ├── appsVulnsAsync.ts
│ │ ├── persistHelpers.ts
│ │ ├── statusAsync.ts
│ │ └── subscriptionAsync.ts
│ └── set
│ │ ├── difference.js
│ │ ├── difference.spec.js
│ │ ├── intersection.js
│ │ └── intersection.spec.js
└── yarn.lock
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.erb/configs/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off",
4 | "global-require": "off",
5 | "import/no-dynamic-require": "off"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Base webpack config used across other specific configs
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import { dependencies as externals } from '../../src/package.json';
8 |
9 | export default {
10 | externals: [...Object.keys(externals || {})],
11 |
12 | module: {
13 | rules: [
14 | {
15 | test: /\.tsx?$/,
16 | exclude: /node_modules/,
17 | use: {
18 | loader: 'babel-loader',
19 | options: {
20 | cacheDirectory: true,
21 | },
22 | },
23 | },
24 | ],
25 | },
26 |
27 | output: {
28 | path: path.join(__dirname, '../../src'),
29 | // https://github.com/webpack/webpack/issues/1114
30 | libraryTarget: 'commonjs2',
31 | },
32 |
33 | /**
34 | * Determine the array of extensions that should be used to resolve modules.
35 | */
36 | resolve: {
37 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
38 | modules: [path.join(__dirname, '../../src'), 'node_modules'],
39 | },
40 |
41 | plugins: [
42 | new webpack.EnvironmentPlugin({
43 | NODE_ENV: 'production',
44 | }),
45 | ],
46 | };
47 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.eslint.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-unresolved: off, import/no-self-import: off */
2 | require('@babel/register');
3 |
4 | module.exports = require('./webpack.config.renderer.dev.babel').default;
5 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.main.prod.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Webpack config for production electron main process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import { merge } from 'webpack-merge';
8 | import TerserPlugin from 'terser-webpack-plugin';
9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
10 | import baseConfig from './webpack.config.base';
11 | import CheckNodeEnv from '../scripts/CheckNodeEnv';
12 | import DeleteSourceMaps from '../scripts/DeleteSourceMaps';
13 |
14 | CheckNodeEnv('production');
15 | DeleteSourceMaps();
16 |
17 | const devtoolsConfig = process.env.DEBUG_PROD === 'true' ? {
18 | devtool: 'source-map'
19 | } : {};
20 |
21 | export default merge(baseConfig, {
22 | ...devtoolsConfig,
23 |
24 | mode: 'production',
25 |
26 | target: 'electron-main',
27 |
28 | entry: './src/main.dev.ts',
29 |
30 | output: {
31 | path: path.join(__dirname, '../../'),
32 | filename: './src/main.prod.js',
33 | },
34 |
35 | optimization: {
36 | minimizer: [
37 | new TerserPlugin({
38 | parallel: true,
39 | }),
40 | ]
41 | },
42 |
43 | plugins: [
44 | new BundleAnalyzerPlugin({
45 | analyzerMode:
46 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
47 | openAnalyzer: process.env.OPEN_ANALYZER === 'true',
48 | }),
49 |
50 | /**
51 | * Create global constants which can be configured at compile time.
52 | *
53 | * Useful for allowing different behaviour between development builds and
54 | * release builds
55 | *
56 | * NODE_ENV should be production so that modules do not perform certain
57 | * development checks
58 | */
59 | new webpack.EnvironmentPlugin({
60 | NODE_ENV: 'production',
61 | DEBUG_PROD: false,
62 | START_MINIMIZED: false,
63 | }),
64 | ],
65 |
66 | /**
67 | * Disables webpack processing of __dirname and __filename.
68 | * If you run the bundle in node.js it falls back to these values of node.js.
69 | * https://github.com/webpack/webpack/issues/2010
70 | */
71 | node: {
72 | __dirname: false,
73 | __filename: false,
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.dev.babel.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'fs';
3 | import webpack from 'webpack';
4 | import chalk from 'chalk';
5 | import { merge } from 'webpack-merge';
6 | import { spawn, execSync } from 'child_process';
7 | import baseConfig from './webpack.config.base';
8 | import CheckNodeEnv from '../scripts/CheckNodeEnv';
9 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
10 |
11 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
12 | // at the dev webpack config is not accidentally run in a production environment
13 | if (process.env.NODE_ENV === 'production') {
14 | CheckNodeEnv('development');
15 | }
16 |
17 | const port = process.env.PORT || 1212;
18 | const publicPath = `http://localhost:${port}/dist`;
19 | const dllDir = path.join(__dirname, '../dll');
20 | const manifest = path.resolve(dllDir, 'renderer.json');
21 | const requiredByDLLConfig = module.parent.filename.includes(
22 | 'webpack.config.renderer.dev.dll'
23 | );
24 |
25 | /**
26 | * Warn if the DLL is not built
27 | */
28 | if (!requiredByDLLConfig && !(fs.existsSync(dllDir) && fs.existsSync(manifest))) {
29 | console.log(
30 | chalk.black.bgYellow.bold(
31 | 'The DLL files are missing. Sit back while we build them for you with "yarn build-dll"'
32 | )
33 | );
34 | execSync('yarn postinstall');
35 | }
36 |
37 | export default merge(baseConfig, {
38 | devtool: 'inline-source-map',
39 |
40 | mode: 'development',
41 |
42 | target: 'electron-renderer',
43 |
44 | entry: [
45 | 'core-js',
46 | 'regenerator-runtime/runtime',
47 | require.resolve('../../src/index.tsx'),
48 | ],
49 |
50 | output: {
51 | publicPath: `http://localhost:${port}/dist/`,
52 | filename: 'renderer.dev.js',
53 | },
54 |
55 | module: {
56 | rules: [
57 | {
58 | test: /\.[jt]sx?$/,
59 | exclude: /node_modules/,
60 | use: [
61 | {
62 | loader: require.resolve('babel-loader'),
63 | options: {
64 | plugins: [
65 | require.resolve('react-refresh/babel'),
66 | ].filter(Boolean),
67 | },
68 | },
69 | ],
70 | },
71 | {
72 | test: /\.global\.css$/,
73 | use: [
74 | {
75 | loader: 'style-loader',
76 | },
77 | {
78 | loader: 'css-loader',
79 | options: {
80 | sourceMap: true,
81 | },
82 | },
83 | ],
84 | },
85 | {
86 | test: /^((?!\.global).)*\.css$/,
87 | use: [
88 | {
89 | loader: 'style-loader',
90 | },
91 | {
92 | loader: 'css-loader',
93 | options: {
94 | modules: {
95 | localIdentName: '[name]__[local]__[hash:base64:5]',
96 | },
97 | sourceMap: true,
98 | importLoaders: 1,
99 | },
100 | },
101 | ],
102 | },
103 | // SASS support - compile all .global.scss files and pipe it to style.css
104 | {
105 | test: /\.global\.(scss|sass)$/,
106 | use: [
107 | {
108 | loader: 'style-loader',
109 | },
110 | {
111 | loader: 'css-loader',
112 | options: {
113 | sourceMap: true,
114 | },
115 | },
116 | {
117 | loader: 'sass-loader',
118 | },
119 | ],
120 | },
121 | // SASS support - compile all other .scss files and pipe it to style.css
122 | {
123 | test: /^((?!\.global).)*\.(scss|sass)$/,
124 | use: [
125 | {
126 | loader: 'style-loader',
127 | },
128 | {
129 | loader: '@teamsupercell/typings-for-css-modules-loader',
130 | },
131 | {
132 | loader: 'css-loader',
133 | options: {
134 | modules: {
135 | localIdentName: '[name]__[local]__[hash:base64:5]',
136 | },
137 | sourceMap: true,
138 | importLoaders: 1,
139 | },
140 | },
141 | {
142 | loader: 'sass-loader',
143 | },
144 | ],
145 | },
146 | // WOFF Font
147 | {
148 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
149 | use: {
150 | loader: 'url-loader',
151 | options: {
152 | limit: 10000,
153 | mimetype: 'application/font-woff',
154 | },
155 | },
156 | },
157 | // WOFF2 Font
158 | {
159 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
160 | use: {
161 | loader: 'url-loader',
162 | options: {
163 | limit: 10000,
164 | mimetype: 'application/font-woff',
165 | },
166 | },
167 | },
168 | // OTF Font
169 | {
170 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/,
171 | use: {
172 | loader: 'url-loader',
173 | options: {
174 | limit: 10000,
175 | mimetype: 'font/otf',
176 | },
177 | },
178 | },
179 | // TTF Font
180 | {
181 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
182 | use: {
183 | loader: 'url-loader',
184 | options: {
185 | limit: 10000,
186 | mimetype: 'application/octet-stream',
187 | },
188 | },
189 | },
190 | // EOT Font
191 | {
192 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
193 | use: 'file-loader',
194 | },
195 | // SVG Font
196 | {
197 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
198 | use: {
199 | loader: 'url-loader',
200 | options: {
201 | limit: 10000,
202 | mimetype: 'image/svg+xml',
203 | },
204 | },
205 | },
206 | // Common Image Formats
207 | {
208 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
209 | use: 'url-loader',
210 | },
211 | ],
212 | },
213 | plugins: [
214 |
215 | requiredByDLLConfig
216 | ? null
217 | : new webpack.DllReferencePlugin({
218 | context: path.join(__dirname, '../dll'),
219 | manifest: require(manifest),
220 | sourceType: 'var',
221 | }),
222 |
223 | new webpack.NoEmitOnErrorsPlugin(),
224 |
225 | /**
226 | * Create global constants which can be configured at compile time.
227 | *
228 | * Useful for allowing different behaviour between development builds and
229 | * release builds
230 | *
231 | * NODE_ENV should be production so that modules do not perform certain
232 | * development checks
233 | *
234 | * By default, use 'development' as NODE_ENV. This can be overriden with
235 | * 'staging', for example, by changing the ENV variables in the npm scripts
236 | */
237 | new webpack.EnvironmentPlugin({
238 | NODE_ENV: 'development',
239 | }),
240 |
241 | new webpack.LoaderOptionsPlugin({
242 | debug: true,
243 | }),
244 |
245 | new ReactRefreshWebpackPlugin(),
246 | ],
247 |
248 | node: {
249 | __dirname: false,
250 | __filename: false,
251 | },
252 |
253 | devServer: {
254 | port,
255 | publicPath,
256 | compress: true,
257 | noInfo: false,
258 | stats: 'errors-only',
259 | inline: true,
260 | lazy: false,
261 | hot: true,
262 | headers: { 'Access-Control-Allow-Origin': '*' },
263 | contentBase: path.join(__dirname, 'dist'),
264 | watchOptions: {
265 | aggregateTimeout: 300,
266 | ignored: /node_modules/,
267 | poll: 100,
268 | },
269 | historyApiFallback: {
270 | verbose: true,
271 | disableDotRule: false,
272 | },
273 | before() {
274 | console.log('Starting Main Process...');
275 | spawn('npm', ['run', 'start:main'], {
276 | shell: true,
277 | env: process.env,
278 | stdio: 'inherit',
279 | })
280 | .on('close', (code) => process.exit(code))
281 | .on('error', (spawnError) => console.error(spawnError));
282 | },
283 | },
284 | });
285 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.dev.dll.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds the DLL for development electron renderer process
3 | */
4 |
5 | import webpack from 'webpack';
6 | import path from 'path';
7 | import { merge } from 'webpack-merge';
8 | import baseConfig from './webpack.config.base';
9 | import { dependencies } from '../../package.json';
10 | import CheckNodeEnv from '../scripts/CheckNodeEnv';
11 |
12 | CheckNodeEnv('development');
13 |
14 | const dist = path.join(__dirname, '../dll');
15 |
16 | export default merge(baseConfig, {
17 | context: path.join(__dirname, '../..'),
18 |
19 | devtool: 'eval',
20 |
21 | mode: 'development',
22 |
23 | target: 'electron-renderer',
24 |
25 | externals: ['fsevents', 'crypto-browserify'],
26 |
27 | /**
28 | * Use `module` from `webpack.config.renderer.dev.js`
29 | */
30 | module: require('./webpack.config.renderer.dev.babel').default.module,
31 |
32 | entry: {
33 | renderer: Object.keys(dependencies || {}),
34 | },
35 |
36 | output: {
37 | library: 'renderer',
38 | path: dist,
39 | filename: '[name].dev.dll.js',
40 | libraryTarget: 'var',
41 | },
42 |
43 | plugins: [
44 | new webpack.DllPlugin({
45 | path: path.join(dist, '[name].json'),
46 | name: '[name]',
47 | }),
48 |
49 | /**
50 | * Create global constants which can be configured at compile time.
51 | *
52 | * Useful for allowing different behaviour between development builds and
53 | * release builds
54 | *
55 | * NODE_ENV should be production so that modules do not perform certain
56 | * development checks
57 | */
58 | new webpack.EnvironmentPlugin({
59 | NODE_ENV: 'development',
60 | }),
61 |
62 | new webpack.LoaderOptionsPlugin({
63 | debug: true,
64 | options: {
65 | context: path.join(__dirname, '../../src'),
66 | output: {
67 | path: path.join(__dirname, '../dll'),
68 | },
69 | },
70 | }),
71 | ],
72 | });
73 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.prod.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build config for electron renderer process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
8 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
9 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
10 | import { merge } from 'webpack-merge';
11 | import TerserPlugin from 'terser-webpack-plugin';
12 | import baseConfig from './webpack.config.base';
13 | import CheckNodeEnv from '../scripts/CheckNodeEnv';
14 | import DeleteSourceMaps from '../scripts/DeleteSourceMaps';
15 |
16 | CheckNodeEnv('production');
17 | DeleteSourceMaps();
18 |
19 | const devtoolsConfig = process.env.DEBUG_PROD === 'true' ? {
20 | devtool: 'source-map'
21 | } : {};
22 |
23 | export default merge(baseConfig, {
24 | ...devtoolsConfig,
25 |
26 | mode: 'production',
27 |
28 | target: 'electron-renderer',
29 |
30 | entry: [
31 | 'core-js',
32 | 'regenerator-runtime/runtime',
33 | path.join(__dirname, '../../src/index.tsx'),
34 | ],
35 |
36 | output: {
37 | path: path.join(__dirname, '../../src/dist'),
38 | publicPath: './dist/',
39 | filename: 'renderer.prod.js',
40 | },
41 |
42 | module: {
43 | rules: [
44 | {
45 | test: /.s?css$/,
46 | use: [
47 | {
48 | loader: MiniCssExtractPlugin.loader,
49 | options: {
50 | // `./dist` can't be inerhited for publicPath for styles. Otherwise generated paths will be ./dist/dist
51 | publicPath: './',
52 | },
53 | },
54 | 'css-loader',
55 | 'sass-loader'
56 | ],
57 | },
58 | // WOFF Font
59 | {
60 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
61 | use: {
62 | loader: 'url-loader',
63 | options: {
64 | limit: 10000,
65 | mimetype: 'application/font-woff',
66 | },
67 | },
68 | },
69 | // WOFF2 Font
70 | {
71 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
72 | use: {
73 | loader: 'url-loader',
74 | options: {
75 | limit: 10000,
76 | mimetype: 'application/font-woff',
77 | },
78 | },
79 | },
80 | // OTF Font
81 | {
82 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/,
83 | use: {
84 | loader: 'url-loader',
85 | options: {
86 | limit: 10000,
87 | mimetype: 'font/otf',
88 | },
89 | },
90 | },
91 | // TTF Font
92 | {
93 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
94 | use: {
95 | loader: 'url-loader',
96 | options: {
97 | limit: 10000,
98 | mimetype: 'application/octet-stream',
99 | },
100 | },
101 | },
102 | // EOT Font
103 | {
104 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
105 | use: 'file-loader',
106 | },
107 | // SVG Font
108 | {
109 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
110 | use: {
111 | loader: 'url-loader',
112 | options: {
113 | limit: 10000,
114 | mimetype: 'image/svg+xml',
115 | },
116 | },
117 | },
118 | // Common Image Formats
119 | {
120 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
121 | use: 'url-loader',
122 | },
123 | ],
124 | },
125 |
126 | optimization: {
127 | minimize: true,
128 | minimizer:
129 | [
130 | new TerserPlugin({
131 | parallel: true,
132 | }),
133 | new CssMinimizerPlugin(),
134 | ],
135 | },
136 |
137 | plugins: [
138 | /**
139 | * Create global constants which can be configured at compile time.
140 | *
141 | * Useful for allowing different behaviour between development builds and
142 | * release builds
143 | *
144 | * NODE_ENV should be production so that modules do not perform certain
145 | * development checks
146 | */
147 | new webpack.EnvironmentPlugin({
148 | NODE_ENV: 'production',
149 | DEBUG_PROD: false,
150 | }),
151 |
152 | new MiniCssExtractPlugin({
153 | filename: 'style.css',
154 | }),
155 |
156 | new BundleAnalyzerPlugin({
157 | analyzerMode:
158 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
159 | openAnalyzer: process.env.OPEN_ANALYZER === 'true',
160 | }),
161 | ],
162 | });
163 |
--------------------------------------------------------------------------------
/.erb/img/erb-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/erb-banner.png
--------------------------------------------------------------------------------
/.erb/img/erb-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/erb-logo.png
--------------------------------------------------------------------------------
/.erb/img/eslint-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/eslint-padded-90.png
--------------------------------------------------------------------------------
/.erb/img/eslint-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/eslint-padded.png
--------------------------------------------------------------------------------
/.erb/img/eslint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/eslint.png
--------------------------------------------------------------------------------
/.erb/img/jest-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/jest-padded-90.png
--------------------------------------------------------------------------------
/.erb/img/jest-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/jest-padded.png
--------------------------------------------------------------------------------
/.erb/img/jest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/jest.png
--------------------------------------------------------------------------------
/.erb/img/js-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/js-padded.png
--------------------------------------------------------------------------------
/.erb/img/js.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/js.png
--------------------------------------------------------------------------------
/.erb/img/npm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/npm.png
--------------------------------------------------------------------------------
/.erb/img/react-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/react-padded-90.png
--------------------------------------------------------------------------------
/.erb/img/react-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/react-padded.png
--------------------------------------------------------------------------------
/.erb/img/react-router-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/react-router-padded-90.png
--------------------------------------------------------------------------------
/.erb/img/react-router-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/react-router-padded.png
--------------------------------------------------------------------------------
/.erb/img/react-router.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/react-router.png
--------------------------------------------------------------------------------
/.erb/img/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/react.png
--------------------------------------------------------------------------------
/.erb/img/webpack-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/webpack-padded-90.png
--------------------------------------------------------------------------------
/.erb/img/webpack-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/webpack-padded.png
--------------------------------------------------------------------------------
/.erb/img/webpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/webpack.png
--------------------------------------------------------------------------------
/.erb/img/yarn-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/yarn-padded-90.png
--------------------------------------------------------------------------------
/.erb/img/yarn-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/yarn-padded.png
--------------------------------------------------------------------------------
/.erb/img/yarn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/.erb/img/yarn.png
--------------------------------------------------------------------------------
/.erb/mocks/fileMock.js:
--------------------------------------------------------------------------------
1 | export default 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/.erb/scripts/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off",
4 | "global-require": "off",
5 | "import/no-dynamic-require": "off",
6 | "import/no-extraneous-dependencies": "off"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.erb/scripts/BabelRegister.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | require('@babel/register')({
4 | extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'],
5 | cwd: path.join(__dirname, '../..'),
6 | });
7 |
--------------------------------------------------------------------------------
/.erb/scripts/CheckBuildsExist.js:
--------------------------------------------------------------------------------
1 | // Check if the renderer and main bundles are built
2 | import path from 'path';
3 | import chalk from 'chalk';
4 | import fs from 'fs';
5 |
6 | const mainPath = path.join(__dirname, '../../src/main.prod.js');
7 | const rendererPath = path.join(
8 | __dirname, '../../src/dist/renderer.prod.js'
9 | );
10 |
11 | if (!fs.existsSync(mainPath)) {
12 | throw new Error(
13 | chalk.whiteBright.bgRed.bold(
14 | 'The main process is not built yet. Build it by running "yarn build:main"'
15 | )
16 | );
17 | }
18 |
19 | if (!fs.existsSync(rendererPath)) {
20 | throw new Error(
21 | chalk.whiteBright.bgRed.bold(
22 | 'The renderer process is not built yet. Build it by running "yarn build:renderer"'
23 | )
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/.erb/scripts/CheckNativeDep.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import chalk from 'chalk';
3 | import { execSync } from 'child_process';
4 | import { dependencies } from '../../package.json';
5 |
6 | if (dependencies) {
7 | const dependenciesKeys = Object.keys(dependencies);
8 | const nativeDeps = fs
9 | .readdirSync('node_modules')
10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`));
11 | if (nativeDeps.length === 0) {
12 | process.exit(0);
13 | }
14 | try {
15 | // Find the reason for why the dependency is installed. If it is installed
16 | // because of a devDependency then that is okay. Warn when it is installed
17 | // because of a dependency
18 | const { dependencies: dependenciesObject } = JSON.parse(
19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
20 | );
21 | const rootDependencies = Object.keys(dependenciesObject);
22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) =>
23 | dependenciesKeys.includes(rootDependency)
24 | );
25 | if (filteredRootDependencies.length > 0) {
26 | const plural = filteredRootDependencies.length > 1;
27 | console.log(`
28 | ${chalk.whiteBright.bgYellow.bold(
29 | 'Webpack does not work with native dependencies.'
30 | )}
31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${
32 | plural ? 'are native dependencies' : 'is a native dependency'
33 | } and should be installed inside of the "./src" folder.
34 | First, uninstall the packages from "./package.json":
35 | ${chalk.whiteBright.bgGreen.bold('yarn remove your-package')}
36 | ${chalk.bold(
37 | 'Then, instead of installing the package to the root "./package.json":'
38 | )}
39 | ${chalk.whiteBright.bgRed.bold('yarn add your-package')}
40 | ${chalk.bold('Install the package to "./src/package.json"')}
41 | ${chalk.whiteBright.bgGreen.bold('cd ./src && yarn add your-package')}
42 | Read more about native dependencies at:
43 | ${chalk.bold(
44 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure'
45 | )}
46 | `);
47 | process.exit(1);
48 | }
49 | } catch (e) {
50 | console.log('Native dependencies could not be checked');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.erb/scripts/CheckNodeEnv.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export default function CheckNodeEnv(expectedEnv) {
4 | if (!expectedEnv) {
5 | throw new Error('"expectedEnv" not set');
6 | }
7 |
8 | if (process.env.NODE_ENV !== expectedEnv) {
9 | console.log(
10 | chalk.whiteBright.bgRed.bold(
11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
12 | )
13 | );
14 | process.exit(2);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.erb/scripts/CheckPortInUse.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import detectPort from 'detect-port';
3 |
4 | const port = process.env.PORT || '1212';
5 |
6 | detectPort(port, (err, availablePort) => {
7 | if (port !== String(availablePort)) {
8 | throw new Error(
9 | chalk.whiteBright.bgRed.bold(
10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn start`
11 | )
12 | );
13 | } else {
14 | process.exit(0);
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/.erb/scripts/DeleteSourceMaps.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import rimraf from 'rimraf';
3 |
4 | export default function deleteSourceMaps() {
5 | rimraf.sync(path.join(__dirname, '../../src/dist/*.js.map'));
6 | rimraf.sync(path.join(__dirname, '../../src/*.js.map'));
7 | }
8 |
--------------------------------------------------------------------------------
/.erb/scripts/ElectronRebuild.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { execSync } from 'child_process';
3 | import fs from 'fs';
4 | import { dependencies } from '../../src/package.json';
5 |
6 | const nodeModulesPath = path.join(__dirname, '../../src/node_modules');
7 |
8 | if (
9 | Object.keys(dependencies || {}).length > 0 &&
10 | fs.existsSync(nodeModulesPath)
11 | ) {
12 | const electronRebuildCmd =
13 | '../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .';
14 | const cmd =
15 | process.platform === 'win32'
16 | ? electronRebuildCmd.replace(/\//g, '\\')
17 | : electronRebuildCmd;
18 | execSync(cmd, {
19 | cwd: path.join(__dirname, '../../src'),
20 | stdio: 'inherit',
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/.erb/scripts/Notarize.js:
--------------------------------------------------------------------------------
1 | const { notarize } = require('electron-notarize');
2 | const { build } = require('../../package.json');
3 |
4 | exports.default = async function notarizeMacos(context) {
5 | const { electronPlatformName, appOutDir } = context;
6 | if (electronPlatformName !== 'darwin') {
7 | return;
8 | }
9 |
10 | // if (!process.env.CI) {
11 | // console.warn('Skipping notarizing step. Packaging is not running in CI');
12 | // return;
13 | // }
14 |
15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {
16 | console.warn('Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set');
17 | return;
18 | }
19 |
20 | const appName = context.packager.appInfo.productFilename;
21 |
22 | await notarize({
23 | appBundleId: build.appId,
24 | appPath: `${appOutDir}/${appName}.app`,
25 | appleId: process.env.APPLE_ID,
26 | appleIdPassword: process.env.APPLE_ID_PASS,
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 | .eslintcache
25 |
26 | # Dependency directory
27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
28 | node_modules
29 |
30 | # OSX
31 | .DS_Store
32 |
33 | # App packaged
34 | release
35 | src/*.main.prod.js
36 | src/main.prod.js
37 | src/main.prod.js.map
38 | src/renderer.prod.js
39 | src/renderer.prod.js.map
40 | src/style.css
41 | src/style.css.map
42 | dist
43 | dll
44 | main.js
45 | main.js.map
46 |
47 | .idea
48 | npm-debug.log.*
49 | __snapshots__
50 |
51 | # Package.json
52 | package.json
53 | .travis.yml
54 | *.css.d.ts
55 | *.sass.d.ts
56 | *.scss.d.ts
57 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'erb',
3 | rules: {
4 | // A temporary hack related to IDE not resolving correct package.json
5 | 'import/no-extraneous-dependencies': 'off',
6 | },
7 | parserOptions: {
8 | ecmaVersion: 2020,
9 | sourceType: 'module',
10 | project: './tsconfig.json',
11 | tsconfigRootDir: __dirname,
12 | createDefaultProgram: true,
13 | },
14 | settings: {
15 | 'import/resolver': {
16 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
17 | node: {},
18 | webpack: {
19 | config: require.resolve('./.erb/configs/webpack.config.eslint.js'),
20 | },
21 | },
22 | 'import/parsers': {
23 | '@typescript-eslint/parser': ['.ts', '.tsx'],
24 | },
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | *.exe binary
3 | *.png binary
4 | *.jpg binary
5 | *.jpeg binary
6 | *.ico binary
7 | *.icns binary
8 | *.eot binary
9 | *.otf binary
10 | *.ttf binary
11 | *.woff binary
12 | *.woff2 binary
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # IDE related stuff
6 | .clj-kondo
7 | .lsp
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 | .eslintcache
29 |
30 | # Dependency directory
31 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
32 | node_modules
33 |
34 | # OSX
35 | .DS_Store
36 |
37 | # App packaged
38 | release
39 | src/main.prod.js
40 | src/main.prod.js.map
41 | src/renderer.prod.js
42 | src/renderer.prod.js.map
43 | src/style.css
44 | src/style.css.map
45 | dist
46 | dll
47 | main.js
48 | main.js.map
49 |
50 | .idea
51 | npm-debug.log.*
52 | *.css.d.ts
53 | *.sass.d.ts
54 | *.scss.d.ts
55 |
56 | *.provisionprofile
57 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "EditorConfig.EditorConfig",
5 | "msjsdiag.debugger-for-chrome"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Electron: Main",
6 | "type": "node",
7 | "request": "launch",
8 | "protocol": "inspector",
9 | "runtimeExecutable": "yarn",
10 | "runtimeArgs": ["start:main --inspect=5858 --remote-debugging-port=9223"],
11 | "preLaunchTask": "Start Webpack Dev"
12 | },
13 | {
14 | "name": "Electron: Renderer",
15 | "type": "chrome",
16 | "request": "attach",
17 | "port": 9223,
18 | "webRoot": "${workspaceFolder}",
19 | "timeout": 15000
20 | }
21 | ],
22 | "compounds": [
23 | {
24 | "name": "Electron: All",
25 | "configurations": ["Electron: Main", "Electron: Renderer"]
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | ".babelrc": "jsonc",
4 | ".eslintrc": "jsonc",
5 | ".prettierrc": "jsonc",
6 | ".eslintignore": "ignore"
7 | },
8 |
9 | "javascript.validate.enable": false,
10 | "javascript.format.enable": false,
11 | "typescript.format.enable": false,
12 |
13 | "search.exclude": {
14 | ".git": true,
15 | ".eslintcache": true,
16 | "src/dist": true,
17 | "src/main.prod.js": true,
18 | "src/main.prod.js.map": true,
19 | "bower_components": true,
20 | "dll": true,
21 | "release": true,
22 | "node_modules": true,
23 | "npm-debug.log.*": true,
24 | "test/**/__snapshots__": true,
25 | "yarn.lock": true,
26 | "*.{css,sass,scss}.d.ts": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "label": "Start Webpack Dev",
7 | "script": "start:renderer",
8 | "options": {
9 | "cwd": "${workspaceFolder}"
10 | },
11 | "isBackground": true,
12 | "problemMatcher": {
13 | "owner": "custom",
14 | "pattern": {
15 | "regexp": "____________"
16 | },
17 | "background": {
18 | "activeOnStart": true,
19 | "beginsPattern": "Compiling\\.\\.\\.$",
20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$"
21 | }
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at electronreactboilerplate@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-present Electron React Boilerplate
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PUBLISH.md:
--------------------------------------------------------------------------------
1 | # How to publish new app versions.
2 |
3 | This manual describes steps to publish new versions of notarized Mana Security's app. Basically, it
4 | requires 4 steps:
5 | 1. (optional) Issue and import necessarry Apple certificates.
6 | 2. (optional) Add APPLE_ID and APPLE_ID_PASS to environment variables.
7 | 3. Build ".app" and zip it.
8 | 4. Publish zip-archive in Mana's admin panel.
9 |
10 | ## 1. Issue and import necessarry Apple certificates
11 | 1. Open https://developer.apple.com/account/resources/certificates/list.
12 | 2. Click "+" to create a new certificate.
13 | 3. Pick "Developer ID Application" option and press "Continue".
14 | 4. Create "Certificate Signing Request" using this instruction: https://help.apple.com/developer-account/#/devbfa00fef7
15 | 5. Get back to the browser and upload CSR.
16 | 6. Apple should offer you an option to download the necessary certificate.
17 | 7. Open it in Finder and add it to the keychain.
18 |
19 | ## 2. Add APPLE_ID and APPLE_ID_PASS to environment variables.
20 | 1. Generate app-specific password for your Apple account here: https://appleid.apple.com.
21 | 2. Add your email to APPLE_ID environment variables.
22 | 3. Add your app-specific password from 2.1 to APPLE_ID_PASS environment variables.
23 |
24 | ## 3. Build ".app" and zip it.
25 | 1. Run `yarn package --mac` in the terminal.
26 | 2. Open `release/mac` folder and zip `.app` file.
27 |
28 | ## 4. Publish zip-archive in Mana's admin panel
29 | 1. Open our admin panel and pick `Electron updates` section.
30 | 2. Press "Add electron update" button.
31 | 3. Fill in necessary fields (version, platform and architecture) and upload the ZIP file from 3.2.
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Mana Security
3 |
4 |
5 |
6 |
7 | [Mana Security](https://www.manasecurity.com) is a vulnerability management tool for macOS.
8 |
9 |
10 |
11 |
12 | ## Features
13 |
14 | - Continious monitoring of 100+ apps against known and potential vulnerabilities.
15 | - **Instant** detection of a new vulnerabilities as soon as they appear in public databases (e.g. CVE).
16 | - Tracks patching velocity and compares it against Mana's community and other benchmarks.
17 |
18 | ## Install
19 |
20 | First, clone the repo via git and install dependencies:
21 |
22 | ```bash
23 | git clone https://github.com/manasecurity/mana-macos
24 | cd mana-macos
25 | yarn
26 | ```
27 |
28 | ## Starting Development
29 |
30 | Start the app in the `dev` environment:
31 |
32 | ```bash
33 | yarn start
34 | ```
35 |
36 | ## Packaging for Production
37 |
38 | To package apps for the local platform:
39 |
40 | ```bash
41 | yarn package
42 | ```
43 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 2.4.0 | :white_check_mark: |
8 |
9 | ## Reporting a Vulnerability
10 |
11 | We kindly ask you to report all security-related bugs to dan@manasecurity.com
12 |
--------------------------------------------------------------------------------
/assets/assets.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: any;
3 | export default content;
4 | }
5 |
6 | declare module '*.png' {
7 | const content: any;
8 | export default content;
9 | }
10 |
11 | declare module '*.jpg' {
12 | const content: any;
13 | export default content;
14 | }
15 |
--------------------------------------------------------------------------------
/assets/back-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-unsigned-executable-memory
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 | com.apple.security.network.client
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/green-tick.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/house.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icon.icns
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/assets/icons/1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/1024x1024.png
--------------------------------------------------------------------------------
/assets/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/128x128.png
--------------------------------------------------------------------------------
/assets/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/16x16.png
--------------------------------------------------------------------------------
/assets/icons/24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/24x24.png
--------------------------------------------------------------------------------
/assets/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/256x256.png
--------------------------------------------------------------------------------
/assets/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/32x32.png
--------------------------------------------------------------------------------
/assets/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/48x48.png
--------------------------------------------------------------------------------
/assets/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/512x512.png
--------------------------------------------------------------------------------
/assets/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/64x64.png
--------------------------------------------------------------------------------
/assets/icons/96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/icons/96x96.png
--------------------------------------------------------------------------------
/assets/no-icon-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/no-icon-app.png
--------------------------------------------------------------------------------
/assets/offline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/pro-plan-promo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/profile.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/suisseintl-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/suisseintl-regular.woff
--------------------------------------------------------------------------------
/assets/suisseintlmono-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/assets/suisseintlmono-regular.woff
--------------------------------------------------------------------------------
/assets/sync.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/tick.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/upgrade-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/vulns.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: off, import/no-extraneous-dependencies: off */
2 |
3 | const developmentEnvironments = ['development', 'test'];
4 |
5 | const developmentPlugins = [require('@babel/plugin-transform-runtime')];
6 |
7 | const productionPlugins = [
8 | require('babel-plugin-dev-expression'),
9 |
10 | // babel-preset-react-optimize
11 | require('@babel/plugin-transform-react-constant-elements'),
12 | require('@babel/plugin-transform-react-inline-elements'),
13 | require('babel-plugin-transform-react-remove-prop-types'),
14 | ];
15 |
16 | module.exports = (api) => {
17 | // See docs about api at https://babeljs.io/docs/en/config-files#apicache
18 |
19 | const development = api.env(developmentEnvironments);
20 |
21 | return {
22 | presets: [
23 | // @babel/preset-env will automatically target our browserslist targets
24 | require('@babel/preset-env'),
25 | require('@babel/preset-typescript'),
26 | [require('@babel/preset-react'), { development }],
27 | ],
28 | plugins: [
29 | // Stage 0
30 | require('@babel/plugin-proposal-function-bind'),
31 |
32 | // Stage 1
33 | require('@babel/plugin-proposal-export-default-from'),
34 | require('@babel/plugin-proposal-logical-assignment-operators'),
35 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }],
36 | [
37 | require('@babel/plugin-proposal-pipeline-operator'),
38 | { proposal: 'minimal' },
39 | ],
40 | [
41 | require('@babel/plugin-proposal-nullish-coalescing-operator'),
42 | { loose: false },
43 | ],
44 | require('@babel/plugin-proposal-do-expressions'),
45 |
46 | // Stage 2
47 | [require('@babel/plugin-proposal-decorators'), { legacy: true }],
48 | require('@babel/plugin-proposal-function-sent'),
49 | require('@babel/plugin-proposal-export-namespace-from'),
50 | require('@babel/plugin-proposal-numeric-separator'),
51 | require('@babel/plugin-proposal-throw-expressions'),
52 |
53 | // Stage 3
54 | require('@babel/plugin-syntax-dynamic-import'),
55 | require('@babel/plugin-syntax-import-meta'),
56 | [require('@babel/plugin-proposal-class-properties'), { loose: false }],
57 | require('@babel/plugin-proposal-json-strings'),
58 |
59 | ...(development ? developmentPlugins : productionPlugins),
60 | ],
61 | };
62 | };
63 |
--------------------------------------------------------------------------------
/resources/osqueryi:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manasecurity/mana-security-app/5dee5de7b1b8105daf51649f50b17cdde99cb1d9/resources/osqueryi
--------------------------------------------------------------------------------
/rootPath.js:
--------------------------------------------------------------------------------
1 | export default function rootPath() {
2 | let path = __dirname;
3 | if (path.endsWith('app.asar')) {
4 | path = path.slice(0, -9);
5 | }
6 | return path;
7 | }
8 |
--------------------------------------------------------------------------------
/src/App.global.css:
--------------------------------------------------------------------------------
1 | /*
2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules
3 | * See https://github.com/webpack-contrib/sass-loader#imports
4 | */
5 | @import './styles/base.css';
6 | @import './styles/dashboard.css';
7 | @import './styles/fonts.css';
8 |
9 | @import '~antd/dist/antd.css';
10 | @import '~tailwindcss';
11 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prefer-stateless-function */
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import { HashRouter as Router, Switch, Route, NavLink } from 'react-router-dom';
5 |
6 | import { Space, Layout, Image } from 'antd';
7 |
8 | // import icon from '../assets/icon.svg';
9 | import './App.global.css';
10 | import Dashboard from './containers/Dashboard';
11 | import VulnerableAppFullInfoContainer from './containers/VulnerableAppFullInfoContainer';
12 | import VulnerableAppsList from './containers/VulnerableAppsList';
13 | import SidebarIcon from './containers/SidebarIcon';
14 | import houseImg from '../assets/house.svg';
15 | import tickImg from '../assets/tick.svg';
16 |
17 | import Debug from './containers/Debug';
18 | import SyncStatus from './containers/SyncStatus';
19 | import osqueryRefreshThunk from './app/osqueryRefreshThunk';
20 | import syncRepoThunk from './app/repoThunk';
21 | import Subscription from './containers/Subscription';
22 | import CodeActivation from './containers/CodeActivation';
23 | import TrialActivation from "./containers/TrialActivation";
24 |
25 | const { Header, Sider, Content } = Layout;
26 |
27 | class App extends React.Component {
28 | componentDidMount() {
29 | setTimeout(() => {
30 | const { osqueryRefreshThunk, syncRepoThunk } = this.props;
31 | osqueryRefreshThunk();
32 | syncRepoThunk();
33 | }, 1000);
34 | }
35 |
36 | render() {
37 | return (
38 |
39 |
43 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
80 |
81 |
88 | home
89 |
90 |
91 |
92 | {/*
93 |
97 |
104 | vulnerabilities
105 |
106 | */}
107 |
108 |
113 |
114 |
121 | subscription
122 |
123 |
124 |
125 | {process.env.NODE_ENV === 'development' &&
126 |
132 |
136 |
143 | debug
144 |
145 |
146 | }
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | {/* */}
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | );
175 | }
176 | }
177 |
178 | const mapDispatchToProps = (dispatch) => {
179 | return {
180 | // dispatching actions returned by action creators
181 | osqueryRefreshThunk: () => dispatch(osqueryRefreshThunk()),
182 | syncRepoThunk: () => dispatch(syncRepoThunk()),
183 | };
184 | };
185 |
186 | export default connect(null, mapDispatchToProps)(App);
187 |
--------------------------------------------------------------------------------
/src/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render } from '@testing-library/react';
4 | import App from '../App';
5 | import { Provider } from 'react-redux';
6 | import { store } from '../app/testStore'
7 |
8 |
9 | describe('App', () => {
10 | // TODO Need to fix this: tests fail due to some bug.
11 | // const AppWrapper = () => {
12 | // return (
13 | //
14 | //
15 | //
16 | // )
17 | // }
18 | // it('should render', () => {
19 | // expect(render()).toBeTruthy();
20 | // });
21 | it('should be true', () => {
22 | expect(true).toBeTruthy()
23 | })
24 | });
25 |
--------------------------------------------------------------------------------
/src/app/osqueryRefreshThunk.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import { createHash } from 'crypto';
3 |
4 | import { OSQUERY_REFRESH_TIMEOUT } from '../configs';
5 |
6 | import { setRepoAndAppsVulns } from '../features/appsVulnerabilities/appsVulnsSlice';
7 | import detectAffectedApps from '../features/appsVulnerabilities/detectAffectedApps';
8 | import recalculateAnalytics from '../features/analyticsSlice/utils/recalculateAnalytics';
9 | import { refreshAnalytics } from '../features/analyticsSlice/analyticsSlice';
10 |
11 | const localAppsChanged = (oldState, newState) => {
12 | if (!oldState) return false;
13 |
14 | return (
15 | oldState.localAppsHash !== newState.localAppsHash ||
16 | oldState.localVulnerableAppsHash !== newState.localVulnerableAppsHash
17 | );
18 | };
19 |
20 | export default function osqueryRefreshThunk() {
21 | return (dispatch, getState) => {
22 | console.debug('starting osquery sync...');
23 |
24 | ipcRenderer
25 | .invoke('osquery:fetch-apps')
26 | .then((result) => {
27 | const { osqueryResponse, error } = result;
28 | if (!error) {
29 | const { appsVulns, analytics } = getState();
30 | const affectedApps = detectAffectedApps({
31 | ...appsVulns,
32 | localApps: osqueryResponse,
33 | });
34 |
35 | const shaLocalApps = createHash('sha1')
36 | .update(JSON.stringify(osqueryResponse))
37 | .digest('hex');
38 | const shaAffectedApps = createHash('sha1')
39 | .update(JSON.stringify(affectedApps))
40 | .digest('hex');
41 |
42 | const newAppsVulns = {
43 | ...appsVulns,
44 | localApps: osqueryResponse,
45 | localVulnerableApps: affectedApps,
46 | localAppsHash: shaLocalApps,
47 | localVulnerableAppsHash: shaAffectedApps,
48 | };
49 |
50 | if (localAppsChanged(appsVulns, newAppsVulns)) {
51 | ipcRenderer.send('persist:update-apps-vulns', newAppsVulns);
52 | }
53 |
54 | const newAnalytics = recalculateAnalytics(analytics, newAppsVulns);
55 | dispatch(setRepoAndAppsVulns(newAppsVulns));
56 | dispatch(refreshAnalytics(newAnalytics));
57 | } else {
58 | console.error('osquery ipc failed');
59 | }
60 | return true;
61 | })
62 | .catch((error) => {
63 | console.error('ipcRenderer exception: %O', error);
64 | });
65 |
66 | return setTimeout(() => {
67 | dispatch(osqueryRefreshThunk());
68 | }, OSQUERY_REFRESH_TIMEOUT);
69 | };
70 | }
71 |
--------------------------------------------------------------------------------
/src/app/persistAppsVulnsMiddleware.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import Store from 'electron-store';
3 | import {
4 | STATE_STORAGE_ANALYTICS_KEY,
5 | STATE_STORAGE_APPSVULNS_KEY,
6 | STATE_STORAGE_STATUS_KEY,
7 | } from '../configs';
8 |
9 | const persistenStore = new Store({
10 | name: 'manaconfig',
11 | fileExtension: 'json',
12 | });
13 |
14 | /**
15 | * Checks if the state of apps' vulns is on the disk.
16 | *
17 | * @returns 'true' if state of apps' vulns is on the disk. 'false' otherwise.
18 | */
19 | export const hasPersistentState = (key) => {
20 | return persistenStore.has(key);
21 | };
22 |
23 | /**
24 | * Loads a state of apps' vulns from the disk.
25 | *
26 | * @returns a state of apps' vulns.
27 | */
28 | export const loadPersistentState = (key) => {
29 | const appState = persistenStore.get(key, false);
30 | return appState;
31 | };
32 |
33 | // const appsVulnsChanged = (oldState, newState) => {
34 | // console.log('persist: oldState hash=%s', oldState.repoColdHash);
35 | // if (!oldState) return false;
36 |
37 | // return (
38 | // oldState.repoColdHash !== newState.repoColdHash ||
39 | // oldState.repoHotHash !== newState.repoHotHash ||
40 | // oldState.localAppsHash !== newState.localAppsHash ||
41 | // oldState.localVulnerableAppsHash !== newState.localVulnerableAppsHash
42 | // );
43 | // };
44 |
45 | /**
46 | * Persists a part of state related to apps' vulns on disk.
47 | */
48 | // eslint-disable-next-line import/prefer-default-export
49 | export const persistAppsVulnsMiddleware = (store) => (next) => (action) => {
50 | const { status: statusOld } = store.getState();
51 |
52 | const result = next(action);
53 |
54 | const { status: statusNew } = store.getState();
55 |
56 | // if (appsVulnsChanged(appsVulnsOld, appsVulns)) {
57 | // console.log('persist:update-apps-vulns');
58 | // ipcRenderer.send('persist:update-apps-vulns', appsVulns);
59 | // }
60 |
61 | // savePersistentState(STATE_STORAGE_APPSVULNS_KEY, appsVulns);
62 | // savePersistentState(STATE_STORAGE_ANALYTICS_KEY, analytics);
63 | if (statusOld && statusOld.firstLaunch !== statusNew.firstLaunch) {
64 | ipcRenderer.send('persist:update-status', statusNew);
65 | }
66 |
67 | return result;
68 | };
69 |
--------------------------------------------------------------------------------
/src/app/persistSubscriptionThunk.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 |
3 | export default function persistSubscriptionThunk() {
4 | return (dispatch, getState) => {
5 | const { subscription, appsVulns } = getState();
6 | ipcRenderer.send('persist:update-subscription', subscription);
7 | ipcRenderer.send('persist:update-apps-vulns', appsVulns);
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, configureStore } from '@reduxjs/toolkit';
2 | import { setupListeners } from '@reduxjs/toolkit/query';
3 |
4 | import { vulnApi } from '../features/appsVulnerabilities/service/vulnApi';
5 | import appsVulnsReducer from '../features/appsVulnerabilities/appsVulnsSlice';
6 | import analyticsSlice from '../features/analyticsSlice/analyticsSlice';
7 | import statusSlice from '../features/statusSlice/statusSlice';
8 | import { persistAppsVulnsMiddleware } from './persistAppsVulnsMiddleware';
9 | import subscriptionSlice from '../features/subscriptionSlice/subscriptionSlice';
10 |
11 | const loggerMiddleware = (storeAPI) => (next) => (action) => {
12 | const result = next(action);
13 | console.log('next state', storeAPI.getState());
14 | return result;
15 | };
16 |
17 | // eslint-disable-next-line import/prefer-default-export
18 | export const store = configureStore({
19 | reducer: combineReducers({
20 | [vulnApi.reducerPath]: vulnApi.reducer,
21 | appsVulns: appsVulnsReducer,
22 | analytics: analyticsSlice,
23 | status: statusSlice,
24 | subscription: subscriptionSlice,
25 | }),
26 | middleware: (getDefaultMiddleware) =>
27 | getDefaultMiddleware({ serializableCheck: false }).concat([
28 | vulnApi.middleware,
29 | // loggerMiddleware,
30 | persistAppsVulnsMiddleware,
31 | ]),
32 | });
33 |
34 | setupListeners(store.dispatch);
35 |
--------------------------------------------------------------------------------
/src/app/testStore.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, createSlice, createStore } from '@reduxjs/toolkit';
2 | import { setupListeners } from '@reduxjs/toolkit/query';
3 |
4 | import { makeStateSeed } from '../features/appsVulnerabilities/appsVulnsSlice';
5 | import { vulnApi } from '../features/appsVulnerabilities/service/vulnApi';
6 |
7 | const dumbAppReducer = createSlice({
8 | name: 'appsVulns',
9 | initialState: makeStateSeed(),
10 | });
11 |
12 | // eslint-disable-next-line import/prefer-default-export
13 | export const store = createStore(
14 | combineReducers({
15 | [vulnApi.reducerPath]: vulnApi.reducer,
16 | appsVulns: dumbAppReducer,
17 | })
18 | );
19 |
20 | setupListeners(store.dispatch);
21 |
--------------------------------------------------------------------------------
/src/components/ActiveHardeningItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const ActiveHardeningItem = (props) => {
5 | const { name, description } = props;
6 | return (
7 |
8 |
9 | {name}
10 | disable
11 |
12 |
{description}
13 |
14 | )
15 | }
16 |
17 | export default ActiveHardeningItem;
18 |
--------------------------------------------------------------------------------
/src/components/DashboardApp.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image } from 'antd';
3 |
4 | import pluralize from 'pluralize';
5 |
6 | import noIconAppImg from '../../assets/no-icon-app.png';
7 |
8 | const DashboardApp = (props) => {
9 | const { imgUrl, appName, appVulnsCount, risk = 'high' } = props;
10 | return (
11 |
12 |
20 |
21 |
22 |
{appName}
23 |
24 | {pluralize('vulnerability', parseInt(appVulnsCount), true)}
25 |
26 |
27 | {/*
*/}
32 |
33 |
34 | );
35 | };
36 |
37 | export default DashboardApp;
38 |
--------------------------------------------------------------------------------
/src/components/DashboardAppPromo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image } from 'antd';
3 |
4 | import proPlanImg from '../../assets/pro-plan-promo.svg';
5 |
6 | const DashboardAppPromo = (props) => {
7 | const { message } = props;
8 | return (
9 |
10 |
18 |
19 |
20 |
{message}
21 |
22 | Start a free 14 day trial, no credit card required
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default DashboardAppPromo;
31 |
--------------------------------------------------------------------------------
/src/components/DashboardOnboarding.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Row, Col } from 'antd';
4 |
5 | const DashboardOnboarding = () => {
6 | return (
7 |
8 |
9 |
13 |
14 |
15 |
16 |
23 |
24 |
25 |
33 |
34 |
35 |
36 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default DashboardOnboarding;
49 |
--------------------------------------------------------------------------------
/src/components/FreePlan.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { Card, Row, Col, Button, Image, Tag, Form, Input, Alert } from 'antd';
3 | import { Link, useHistory } from 'react-router-dom';
4 |
5 | import greenTickImg from '../../assets/green-tick.svg';
6 |
7 | const FreePlan = (props) => {
8 | const history = useHistory();
9 | const handleOnClick = () => history.push('/trial-activation');
10 |
11 | return (
12 |
13 |
14 |
Your subscription
15 |
16 |
17 |
18 |
19 |
20 | PRO · $59.99/year
21 | Supports 100+ macOS apps
22 | Priority email support
23 | Cancel any time
24 | No credit card required
25 |
26 |
34 |
35 |
36 | Add activation code
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Community · $0
45 |
52 |
53 | Supports 10 essential macOS apps
54 | Email support
55 |
56 |
57 |
58 |
59 | );
60 | };
61 | export default FreePlan;
62 |
--------------------------------------------------------------------------------
/src/components/InactiveHardeningItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const InactiveHardeningItem = (props) => {
5 | const { name, description } = props;
6 | return (
7 |
8 |
9 | {name}
10 | enable
11 |
12 |
{description}
13 |
14 | );
15 | };
16 |
17 | export default InactiveHardeningItem;
18 |
--------------------------------------------------------------------------------
/src/components/NoVulnsSummary.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Col, Image, Row } from 'antd';
3 | import { Link } from 'react-router-dom';
4 |
5 | import greenTickImg from '../../assets/green-tick.svg';
6 | import DashboardAppPromo from './DashboardAppPromo';
7 |
8 | const NoVulnsSummary = (props) => {
9 | const { paid } = props;
10 | if (!paid) {
11 | const promoMessage =
12 | 'Only 10 essential apps are covered by Community edition. Find vulnerabilities for 100+ apps with PRO subscription';
13 | return (
14 |
15 |
16 |
17 |
18 | Essential apps are up-to-date
19 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | return (
40 |
41 |
42 |
43 |
44 | All apps are up-to-date
45 |
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default NoVulnsSummary;
60 |
--------------------------------------------------------------------------------
/src/components/PaidPlan.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, Row, Col, Button, Image, Tag } from 'antd';
3 |
4 | import greenTickImg from '../../assets/green-tick.svg';
5 |
6 | const PaidPlan = (props) => {
7 | const { switchToFreePlan } = props;
8 | return (
9 |
10 |
11 |
Your subscription
12 |
13 |
14 |
15 |
16 |
17 |
18 | PRO · $59.99/year
19 |
26 |
27 | Supports 100+ macOS apps
28 | Priority email support
29 | Cancel any time
30 | No credit card required
31 | {/*
32 |
39 |
*/}
40 |
41 |
42 |
43 |
44 | Community · $0
45 | Supports 10 essential macOS apps
46 | Email support
47 |
48 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 | export default PaidPlan;
63 |
--------------------------------------------------------------------------------
/src/components/VulnerabilityInfo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card } from 'antd';
3 | import PropTypes from 'prop-types';
4 |
5 |
6 | const VulnerabilityInfo = (props) => {
7 | // eslint-disable-next-line @typescript-eslint/naming-convention
8 | const { vulnIndex, description } = props;
9 |
10 | return (
11 |
12 | Vulnerability {vulnIndex}
13 | {description}
14 |
15 | );
16 | };
17 |
18 | export default VulnerabilityInfo;
19 |
--------------------------------------------------------------------------------
/src/components/VulnerableAppShortInfo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router-dom';
4 |
5 | const VulnerableAppShortInfo = (props) => {
6 | // eslint-disable-next-line @typescript-eslint/naming-convention
7 | const { app_name, current_version, solution, cpe_id } = props;
8 |
9 | return (
10 |
11 |
12 | App: {app_name}, version {current_version}
13 |
14 |
Fixed version: {solution}
15 |
Details
16 |
17 | );
18 | };
19 |
20 | VulnerableAppShortInfo.propTypes = {
21 | app_name: PropTypes.string.isRequired,
22 | cpe_id: PropTypes.string.isRequired,
23 | current_version: PropTypes.string.isRequired,
24 | solution: PropTypes.string.isRequired,
25 | };
26 |
27 | export default VulnerableAppShortInfo;
28 |
--------------------------------------------------------------------------------
/src/components/VulnsSummary.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import pluralize from 'pluralize';
4 |
5 | const VulnsSummary = (props) => {
6 | const { affectAppsCount } = props;
7 | return (
8 |
9 |
10 |
11 |
12 | {pluralize('app', affectAppsCount, true)}{' '}
13 | {affectAppsCount === 1 ? 'needs' : 'need'} an update
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | VulnsSummary.propTypes = {
22 | affectAppsCount: PropTypes.number.isRequired,
23 | };
24 |
25 | export default VulnsSummary;
26 |
--------------------------------------------------------------------------------
/src/configs.js:
--------------------------------------------------------------------------------
1 | export const API_HOST = 'https://slack.manasecurity.com/';
2 |
3 | export const ANALYTICS_STATE_SEED = {
4 | // Next three fields are used both in the interface and CSS classes to display stats.
5 | overallRisk: 'low',
6 | velocityRisk: 'low',
7 | quantityRisk: 'low',
8 |
9 | // Next 5 fields are used in the interface.
10 | currentVulns: 0,
11 | last30dVulns: 0,
12 | patchVelocity: 0, // in seconds
13 | benchmarkVelocity: 1 * 24 * 60 * 60, // one day in seconds
14 | otherUsersVelocity: 3 * 24 * 60 * 60, // 3 days in seconds
15 |
16 | // Last two fields are used to calculate stats above.
17 | updateHistory: [], // Recently patched apps appear in the beginning.
18 | localAffectedAppsForAnalytics: {},
19 | };
20 |
21 | export const STATUS_STATE_SEED = {
22 | syncInProgress: false,
23 | online: true,
24 | firstLaunch: true,
25 | };
26 |
27 | export const SUBSCRIPTION_STATE_SEED = {
28 | paid: false,
29 | key: '',
30 | };
31 |
32 | export const STATE_STORAGE_STATUS_KEY = 'status';
33 | export const STATE_STORAGE_ANALYTICS_KEY = 'analytics';
34 | export const STATE_STORAGE_APPSVULNS_KEY = 'appsVulns';
35 | export const STATE_STORAGE_SUBSCRIPTION_KEY = 'subscription';
36 |
37 | // How much time osquery can run. After that the process will be terminated. Timeout is set in
38 | // milliseconds.
39 | export const OSQUERY_RUN_TIMEOUT = 5 * 1000; // 5 seconds
40 | export const OSQUERY_REFRESH_TIMEOUT = 60 * 1000; // each minute
41 |
--------------------------------------------------------------------------------
/src/containers/AppsWidget.tsx:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'antd';
2 | import { useState } from 'hoist-non-react-statics/node_modules/@types/react';
3 | import React from 'react';
4 | import { useSelector } from 'react-redux';
5 | import { Link } from 'react-router-dom';
6 |
7 | import DashboardApp from '../components/DashboardApp';
8 | import DashboardAppPromo from '../components/DashboardAppPromo';
9 |
10 | const AppsWidget = () => {
11 | // eslint-disable-next-line
12 | const { appsRepo, localVulnerableApps } = useSelector((state) => state.appsVulns);
13 | const { paid } = useSelector((state) => state.subscription);
14 |
15 | const vulns = Object.entries(localVulnerableApps).map(([cpeKey, vuln]) => {
16 | const { icon, app_name } = appsRepo[cpeKey];
17 | const iconAsStr = icon ? `${icon}` : '';
18 | const appName = app_name;
19 | const vulnsCount = Object.keys(vuln.vulns).length;
20 | return {
21 | cpeKey,
22 | icon: iconAsStr,
23 | appName,
24 | vulnsCount,
25 | };
26 | });
27 |
28 | // If a user do not have a paid subscription, then we should add a promo in the widget.
29 | const blocksCount = paid ? vulns.length : vulns.length + 1;
30 | const additionalBlockSize = 3 - (blocksCount % 3);
31 |
32 | console.log(`blocks: 1st=${8 * (1 + additionalBlockSize)}`);
33 |
34 | let renderedVulns = vulns.map((vuln, index) => {
35 | // If we can't show 3 items in the last row, then the 1st app should fill the extra space.
36 | if (index === 0 && additionalBlockSize % 3) {
37 | // Calculate how many blocks are missed in the widget. Basically, there're just two options:
38 | // one and two blocks. E.g. if a remainder is 2 then we need to expand the size of the 1st app
39 | // on 1 block. And if the remainder is 1 - expand the app on 2 blocks.
40 | return (
41 |
42 |
43 |
49 |
50 |
51 | );
52 | }
53 |
54 | return (
55 |
56 |
57 |
63 |
64 |
65 | );
66 | });
67 |
68 | if (!paid) {
69 | renderedVulns = renderedVulns.concat(
70 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
78 | return (
79 |
80 | {renderedVulns}
81 |
82 | );
83 | };
84 |
85 | export default AppsWidget;
86 |
--------------------------------------------------------------------------------
/src/containers/CodeActivation.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Input, Alert } from 'antd';
2 | import { ipcRenderer } from 'electron';
3 | import React from 'react';
4 |
5 | import { useDispatch } from 'react-redux';
6 | import { useHistory } from 'react-router';
7 | import persistSubscriptionThunk from '../app/persistSubscriptionThunk';
8 | import syncRepoThunk from '../app/repoThunk';
9 | import { setKey } from '../features/subscriptionSlice/subscriptionSlice';
10 |
11 | const CodeActivation = () => {
12 | const dispatch = useDispatch();
13 | const history = useHistory();
14 |
15 | const setNewKey = (event) => {
16 | const newKey = event.target['new-key'].value;
17 | dispatch(setKey({ key: newKey }));
18 | dispatch(persistSubscriptionThunk());
19 | dispatch(syncRepoThunk(false));
20 | history.push('/subscription');
21 | };
22 |
23 | const openSupportUrl = () => {
24 | ipcRenderer.send('url:send-email-to-support-no-activation-code');
25 | };
26 |
27 | return (
28 |
29 |
30 |
Activate subscription
31 |
32 |
33 |
66 |
67 | );
68 | };
69 |
70 | export default CodeActivation;
71 |
--------------------------------------------------------------------------------
/src/containers/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 |
4 | import { Row, Col } from 'antd';
5 |
6 | import AppsWidget from './AppsWidget';
7 | import DashboardTitle from './DashboardTitle';
8 | import VulnsSummary from '../components/VulnsSummary';
9 | import NoVulnsSummary from '../components/NoVulnsSummary';
10 | import DashboardOnboarding from '../components/DashboardOnboarding';
11 |
12 | const secondsToHumanDays = (seconds, prefix = '') => {
13 | const days = seconds / (60 * 60 * 24);
14 | return days < 1 ? '< 1' : `${prefix}${Math.round(days)}`;
15 | };
16 |
17 | const normaliseDataset = (iterable) => {
18 | const max = Math.max(...iterable);
19 | return iterable.map((entry) => entry / max * 100);
20 | };
21 |
22 | const Dashboard = () => {
23 | const { firstLaunch } = useSelector((state) => state.status);
24 | const { paid } = useSelector((state) => state.subscription);
25 |
26 | const hostAffectedApps = useSelector(
27 | (state) => state.appsVulns.localVulnerableApps
28 | );
29 | const {
30 | overallRisk,
31 | velocityRisk,
32 | quantityRisk,
33 | benchmarkVelocity,
34 | otherUsersVelocity,
35 | patchVelocity,
36 | last30dVulns,
37 | currentVulns,
38 | } = useSelector((state) => state.analytics);
39 | const affectAppsCount = Object.keys(hostAffectedApps).length;
40 |
41 | if (firstLaunch) {
42 | return ;
43 | }
44 |
45 | const overallRiskLevel = overallRisk;
46 | const vulnQuantRiskLevel = quantityRisk;
47 | const patchVeloRiskLevel = velocityRisk;
48 |
49 | // Calculating bar sizes for vulnerability quantity.
50 | const [nCurrentVulns, nLast30dVulns] = normaliseDataset([
51 | currentVulns,
52 | last30dVulns,
53 | ]);
54 |
55 | // Calculating bar sizes for patching velocity.
56 | const [nPatchVelocity, nOtherUsersVelocity, nBenchmarkVelocity] =
57 | normaliseDataset([patchVelocity, otherUsersVelocity, benchmarkVelocity]);
58 |
59 | return (
60 |
61 |
62 |
69 |
70 |
71 |
72 |
73 |
74 |
Vulnerabilities
75 |
76 |
77 | {currentVulns} · Now
78 |
79 |
83 |
{last30dVulns} · Average
84 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | Update speed in days
95 |
96 |
97 |
98 | {secondsToHumanDays(patchVelocity, '>')} · You
99 |
100 |
104 |
105 | {secondsToHumanDays(otherUsersVelocity)} · Community
106 |
107 |
111 |
112 | {secondsToHumanDays(benchmarkVelocity)} · Benchmark
113 |
114 |
118 |
119 |
120 |
121 |
122 |
123 | {currentVulns > 0 ? (
124 |
125 | ) : (
126 |
127 | )}
128 |
129 | {currentVulns > 0 ?
: ''}
130 |
131 | );
132 | };
133 |
134 | export default Dashboard;
135 |
--------------------------------------------------------------------------------
/src/containers/DashboardTitle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import pluralize from 'pluralize';
4 |
5 | const makeSubtitleText = (
6 | vulnQuantRiskLevel,
7 | patchVeloRiskLevel,
8 | vulnQuant,
9 | patchVelo
10 | ) => {
11 | let allOkMsg = `Your Mac's security level is fantastic!
12 | Have a nice day!`;
13 | let vulnMsg = '';
14 | let patchMsg = '';
15 |
16 | if (vulnQuantRiskLevel === 'high') {
17 | vulnMsg = `Number of affected apps is extremely high.`;
18 | } else if (vulnQuantRiskLevel === 'medium') {
19 | vulnMsg = `
20 | There ${pluralize('is', vulnQuant, false)}
21 | ${pluralize('app', vulnQuant, true)}
22 | with a pending security patch.
23 | `;
24 | }
25 |
26 | if (patchVeloRiskLevel === 'high') {
27 | patchMsg = `Patch installation velocity has decreased a lot.`;
28 | } else if (patchVeloRiskLevel === 'medium') {
29 | patchMsg = `Patch installation velocity could be better.`;
30 | }
31 |
32 | return vulnMsg || patchMsg ? `${vulnMsg} ${patchMsg}` : allOkMsg;
33 | };
34 |
35 | const DashboardTitle = (props) => {
36 | const {
37 | overallRiskLevel,
38 | vulnQuantRiskLevel,
39 | patchVeloRiskLevel,
40 | vulnQuant,
41 | patchVelo,
42 | } = props;
43 | const subtitle = makeSubtitleText(
44 | vulnQuantRiskLevel,
45 | patchVeloRiskLevel,
46 | vulnQuant,
47 | patchVelo
48 | );
49 | return (
50 |
51 |
54 | {overallRiskLevel} risk level
55 |
56 |
{subtitle}
57 |
58 | );
59 | };
60 |
61 | export default DashboardTitle;
62 |
--------------------------------------------------------------------------------
/src/containers/Debug.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import beautify from 'js-beautify';
4 | import { Tabs } from 'antd';
5 |
6 | const { TabPane } = Tabs;
7 |
8 | const Debug = () => {
9 | const localVulnApps = useSelector((state) => state.appsVulns.localVulnerableApps);
10 | const localApps = useSelector((state) => state.appsVulns.localApps);
11 | const appsRepo = useSelector((state) => state.appsVulns.appsRepo);
12 |
13 | const localVulnAppsJSON = beautify(JSON.stringify(localVulnApps), {
14 | indent_size: 2,
15 | });
16 | const localAppsJSON = beautify(JSON.stringify(localApps), {
17 | indent_size: 2,
18 | });
19 | const appsRepoJSON = beautify(JSON.stringify(appsRepo), {
20 | indent_size: 2,
21 | });
22 |
23 | return (
24 |
25 |
Debug React State
26 |
27 |
28 |
31 |
32 |
33 |
36 |
37 |
38 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default Debug;
48 |
--------------------------------------------------------------------------------
/src/containers/Hardening.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ActiveHardeningItem from '../components/ActiveHardeningItem';
3 | import InactiveHardeningItem from '../components/InactiveHardeningItem';
4 |
5 | const Hardening = () => {
6 | return (
7 |
8 |
Recommended hardening policies
9 |
13 |
17 |
21 |
22 | );
23 | };
24 |
25 | export default Hardening;
26 |
--------------------------------------------------------------------------------
/src/containers/SidebarIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Route, Switch, Link, useHistory } from 'react-router-dom';
4 |
5 | import { Image } from 'antd';
6 |
7 | import backImg from '../../assets/back-arrow.svg';
8 |
9 | const SidebarIcon = () => {
10 | const { overallRisk } = useSelector((state) => state.analytics);
11 | const overallRiskLevel = overallRisk;
12 |
13 | const history = useHistory();
14 |
15 | return (
16 |
17 |
18 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
36 |
37 |
38 |
39 |
40 |
47 |
48 |
49 |
50 |
51 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default SidebarIcon;
66 |
--------------------------------------------------------------------------------
/src/containers/Subscription.tsx:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import React from 'react';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import persistSubscriptionThunk from '../app/persistSubscriptionThunk';
5 |
6 | import FreePlan from '../components/FreePlan';
7 | import PaidPlan from '../components/PaidPlan';
8 | import { resetKey } from '../features/subscriptionSlice/subscriptionSlice';
9 |
10 | const Subscription = () => {
11 | const { paid } = useSelector((state) => state.subscription);
12 | const dispatch = useDispatch();
13 | if (paid) {
14 | const switchToFreePlan = () => {
15 | dispatch(resetKey());
16 | dispatch(persistSubscriptionThunk());
17 | };
18 | return ;
19 | }
20 |
21 | const openBuyLink = () => ipcRenderer.send('url:open-buy-link');
22 | return ;
23 | };
24 |
25 | export default Subscription;
26 |
--------------------------------------------------------------------------------
/src/containers/SyncStatus.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useSelector } from 'react-redux';
4 |
5 | import { Image } from 'antd';
6 |
7 | import offlineImg from '../../assets/offline.svg';
8 | import syncImg from '../../assets/sync.svg';
9 |
10 | const SyncStatus = () => {
11 | const appState = useSelector((state) => state);
12 | const isOnline = appState?.vulnApi?.config?.online;
13 | const isSync = appState?.status?.syncInProgress;
14 |
15 | if (!isOnline) {
16 | return (
17 |
18 |
25 | No internet
26 |
27 | );
28 | }
29 |
30 | if (isSync) {
31 | return (
32 |
33 |
40 | Synchronizing...
41 |
42 | );
43 | }
44 |
45 | return ;
46 | };
47 |
48 | export default SyncStatus;
49 |
--------------------------------------------------------------------------------
/src/containers/TrialActivation.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Input } from 'antd';
2 | import { ipcRenderer } from 'electron';
3 | import React from 'react';
4 |
5 | import { useHistory } from 'react-router';
6 |
7 | const TrialActivation = () => {
8 | const history = useHistory();
9 |
10 | const getTrial = (event) => {
11 | const email = event.target.email.value;
12 | ipcRenderer.send('url:get-trial-link', { email });
13 | history.push('/code-activation');
14 | };
15 |
16 | return (
17 |
18 |
19 |
Start trial
20 |
21 |
54 |
55 | );
56 | };
57 |
58 | export default TrialActivation;
59 |
--------------------------------------------------------------------------------
/src/containers/VulnerableAppFullInfo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col, Card, Button } from 'antd';
3 | import { shell } from 'electron';
4 | import pluralize from 'pluralize';
5 | import ReactMarkdown from 'react-markdown';
6 |
7 | import VulnerabilityInfo from '../components/VulnerabilityInfo';
8 |
9 | const VulnerableAppFullInfo = (props) => {
10 | // eslint-disable-next-line
11 | const { app_name, cpe, current_version, solution, path, allVulns, howToUpdate } = props;
12 | // eslint-disable-next-line
13 | const renderedVulns = Object.entries(allVulns).map(([vulnId, x], index) => {
14 | return (
15 |
16 |
17 |
18 | );
19 | });
20 |
21 | const showAppInFinder = () => {
22 | console.log('yo');
23 | shell.showItemInFolder(path);
24 | };
25 |
26 | return (
27 |
28 |
29 |
{app_name}
30 |
31 | {pluralize('vulnerability', Object.entries(allVulns).length, true)}
32 |
33 | {/*
34 |
41 |
*/}
42 |
43 |
44 |
45 |
46 |
47 | {solution}
48 |
49 | {'*How to install the update:*\n'.concat(howToUpdate)}
50 |
51 |
58 |
59 |
60 | {renderedVulns}
61 |
62 |
63 | );
64 | };
65 |
66 | export default VulnerableAppFullInfo;
67 |
--------------------------------------------------------------------------------
/src/containers/VulnerableAppFullInfoContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useParams, Redirect } from 'react-router';
3 | import { useSelector } from 'react-redux';
4 |
5 | import VulnerableAppFullInfo from './VulnerableAppFullInfo';
6 |
7 | const VulnerableAppFullInfoContainer = () => {
8 | const { id } = useParams();
9 | const affectedLocalApp = useSelector(
10 | (state) => state.appsVulns.localVulnerableApps[id]
11 | );
12 | const affectedApp = useSelector((state) => state.appsVulns.appsRepo[id]);
13 | if (!affectedLocalApp) return ;
14 |
15 | const { operator } = Object.values(affectedLocalApp.vulns)[0];
16 | const maxAffectedVersion = Object.values(affectedLocalApp.vulns)[0]
17 | .last_version;
18 | const vulnDescription = Object.values(affectedLocalApp.vulns)[0].description;
19 |
20 | if (operator === '<=') {
21 | const solution = `Upgrade to version later than ${maxAffectedVersion}`;
22 | return (
23 |
34 | );
35 | }
36 |
37 | const solution = `update to version ${maxAffectedVersion} or newer`;
38 | return (
39 |
50 | );
51 | };
52 |
53 | export default VulnerableAppFullInfoContainer;
54 |
--------------------------------------------------------------------------------
/src/containers/VulnerableAppShortInfoContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import VulnerableAppShortInfo from '../components/VulnerableAppShortInfo';
4 |
5 | const VulnerableAppShortInfoContainer = (props) => {
6 | const { hostAffectedApps, appsRepo } = props;
7 | const vulnItems = Object.entries(hostAffectedApps).map(([key, val], idx) => {
8 | const maxVersionOperator = Object.values(val.vulns)[0].operator;
9 | const fixedVersion = Object.values(val.vulns)[0].last_version;
10 |
11 | if (maxVersionOperator === '<=') {
12 | const solution = `later than ${fixedVersion}`;
13 | return (
14 |
21 | );
22 | }
23 |
24 | const solution = `${fixedVersion} or later`;
25 | return (
26 |
33 | );
34 | });
35 |
36 | return {vulnItems}
;
37 | };
38 |
39 | export default VulnerableAppShortInfoContainer;
40 |
--------------------------------------------------------------------------------
/src/containers/VulnerableAppsList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 |
4 | import VulnerableAppShortInfoContainer from './VulnerableAppShortInfoContainer';
5 |
6 | const VulnerableAppsList = () => {
7 | const hostAffectedApps = useSelector(
8 | (state) => state.appsVulns.localVulnerableApps
9 | );
10 | const appsRepo = useSelector((state) => state.appsVulns.appsRepo);
11 |
12 | return (
13 |
14 |
Vulnerable apps on your Mac
15 |
19 |
20 | );
21 | };
22 |
23 | export default VulnerableAppsList;
24 |
--------------------------------------------------------------------------------
/src/features/analyticsSlice/analyticsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | import {
4 | ANALYTICS_STATE_SEED,
5 | STATE_STORAGE_ANALYTICS_KEY,
6 | } from '../../configs';
7 | import refreshStats from './utils/refreshStats';
8 | import {
9 | hasPersistentState,
10 | loadPersistentState,
11 | } from '../../app/persistAppsVulnsMiddleware';
12 |
13 | export const loadState = (seed, resetCachedState = false) => {
14 | // Try to restore a state from a cache.
15 | let recoveredState = seed;
16 | if (!resetCachedState && hasPersistentState(STATE_STORAGE_ANALYTICS_KEY)) {
17 | recoveredState = loadPersistentState(STATE_STORAGE_ANALYTICS_KEY);
18 | }
19 |
20 | // Refresh analytical stats after last save.
21 | const {
22 | overallRisk,
23 | velocityRisk,
24 | quantityRisk,
25 | currentVulns,
26 | last30dVulns,
27 | patchVelocity,
28 | } = refreshStats(recoveredState);
29 | return {
30 | ...recoveredState,
31 | overallRisk,
32 | velocityRisk,
33 | quantityRisk,
34 | currentVulns,
35 | last30dVulns,
36 | patchVelocity,
37 | };
38 | };
39 |
40 | const analyticsSlice = createSlice({
41 | name: 'appsVulns',
42 | initialState: loadState(ANALYTICS_STATE_SEED),
43 | reducers: {
44 | /**
45 | * Recalculate analytics for a new list of local affected apps.
46 | */
47 | refreshAnalytics(state, { payload }) {
48 | state.overallRisk = payload.overallRisk;
49 | state.velocityRisk = payload.velocityRisk;
50 | state.quantityRisk = payload.quantityRisk;
51 | state.currentVulns = payload.currentVulns;
52 | state.last30dVulns = payload.last30dVulns;
53 | state.patchVelocity = payload.patchVelocity;
54 | state.benchmarkVelocity = payload.benchmarkVelocity;
55 | state.otherUsersVelocity = payload.otherUsersVelocity;
56 | state.updateHistory = payload.updateHistory;
57 | state.localAffectedAppsForAnalytics = payload.localAffectedAppsForAnalytics;
58 | },
59 | },
60 | });
61 |
62 | export const { refreshAnalytics } = analyticsSlice.actions;
63 | export default analyticsSlice.reducer;
64 |
--------------------------------------------------------------------------------
/src/features/analyticsSlice/utils/recalculateAnalytics.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import produce from 'immer';
3 |
4 | import difference from '../../../utils/set/difference';
5 | import intersection from '../../../utils/set/intersection';
6 | import nowUnixTime from '../../appsVulnerabilities/nowUnixTime';
7 | import refreshStats from './refreshStats';
8 |
9 | export default function recalculateAnalytics(oldState, payload) {
10 | const newState = produce(oldState, (draft) => {
11 | // Timestamp for new vulns or fixed vulns.
12 | const nowUtc = nowUnixTime();
13 |
14 | const { localVulnerableApps } = payload;
15 | const oldAffectedAppsSet = Object.keys(
16 | oldState.localAffectedAppsForAnalytics
17 | ).reduce((set, x) => set.add(x), new Set());
18 | const newAffectedAppsSet = Object.keys(localVulnerableApps).reduce(
19 | (set, x) => set.add(x),
20 | new Set()
21 | );
22 |
23 | // Move patched apps to history.
24 | const patchedApps = difference(oldAffectedAppsSet, newAffectedAppsSet);
25 | const patchedAnalytApps = patchedApps.values().reduce((buff, x) => {
26 | try {
27 | const tmp = {
28 | ...oldState.localAffectedAppsForAnalytics[x],
29 | patched: nowUtc,
30 | };
31 | buff.unshift(tmp);
32 | return buff;
33 | } catch(e) {
34 | console.error("Could not find patched app '%O'. Error: %O", x, e);
35 | return buff;
36 | }
37 | // tmp.patched = nowUtc;
38 | // tmp.patchedVersion = payload.appsRepo[x].current_version;
39 | }, []);
40 | if (patchedAnalytApps.length > 0) {
41 | draft.updateHistory = oldState.updateHistory.concat(patchedAnalytApps);
42 | patchedAnalytApps.forEach((entry) => {
43 | const { cpe, appName } = entry;
44 | ipcRenderer.send('vulns-notification', {
45 | title: `${appName} has been successfully patched`,
46 | body: 'Good job!',
47 | });
48 | });
49 | }
50 |
51 | // Keep existing flawed apps.
52 | const existingAffectedApps = intersection(
53 | oldAffectedAppsSet,
54 | newAffectedAppsSet
55 | );
56 | const existingAnalytApps = existingAffectedApps
57 | .values()
58 | .reduce((buff, x) => {
59 | buff[x] = oldState.localAffectedAppsForAnalytics[x];
60 | return buff;
61 | }, {});
62 |
63 | // Add new affected apps to state.
64 | const newUniqueAffectedApps = difference(
65 | newAffectedAppsSet,
66 | oldAffectedAppsSet
67 | );
68 | const newAnalytApps = newUniqueAffectedApps.values().reduce((buff, x) => {
69 | buff[x] = {
70 | appName: payload.appsRepo[x].app_name,
71 | appeared: nowUtc,
72 | version: localVulnerableApps[x].currentVersion,
73 | patchedVersion: payload.appsRepo[x].current_version,
74 | cpe: x,
75 | };
76 | const appName = payload.appsRepo[x].app_name;
77 | ipcRenderer.send('vulns-notification', {
78 | title: `${appName} has a new vulnerability`,
79 | body: `Update it to the latest version`,
80 | });
81 | return buff;
82 | }, {});
83 |
84 | // Compose new affected apps set from intersection and newly added flawed apps.
85 | draft.localAffectedAppsForAnalytics = {
86 | ...existingAnalytApps,
87 | ...newAnalytApps,
88 | };
89 |
90 | // Recalculate velocity.
91 | const stats = refreshStats(draft);
92 | const {
93 | overallRisk,
94 | velocityRisk,
95 | quantityRisk,
96 | currentVulns,
97 | last30dVulns,
98 | patchVelocity,
99 | } = stats;
100 | draft.overallRisk = overallRisk;
101 | draft.velocityRisk = velocityRisk;
102 | draft.quantityRisk = quantityRisk;
103 | draft.currentVulns = currentVulns;
104 | draft.last30dVulns = last30dVulns;
105 | draft.patchVelocity = patchVelocity;
106 | });
107 |
108 | ipcRenderer.send('persist:update-analytics', newState);
109 | return newState;
110 | }
111 |
--------------------------------------------------------------------------------
/src/features/analyticsSlice/utils/refreshStats.js:
--------------------------------------------------------------------------------
1 | import nowUnixTime from '../../appsVulnerabilities/nowUnixTime';
2 |
3 | const timeDelta = (a, b) => {
4 | if (a > b) {
5 | return a - b;
6 | }
7 |
8 | return 0;
9 | };
10 |
11 | const findFirstPatchedApp30DaysAgo = (historyVulns, days30agoUT = false) => {
12 | let days30ago = days30agoUT;
13 | if (!days30agoUT) days30ago = nowUnixTime() - 30 * 24 * 60 * 60;
14 |
15 | const index = historyVulns.findIndex((x) => x.patched < days30ago);
16 | return index === -1 ? historyVulns.length : index;
17 | };
18 |
19 | export const measurePatchVelocity = (nowVulns, historyVulns, nowUT = false) => {
20 | let nowUtc = nowUT;
21 | if (!nowUT) nowUtc = nowUnixTime();
22 |
23 | const nowVelocityPerApp = Object.values(nowVulns).map((entry) => timeDelta(nowUtc, entry.appeared));
24 |
25 | // History apps sorted from most recently patched to oldest.
26 | const lastApp = findFirstPatchedApp30DaysAgo(historyVulns);
27 | const historyVelocityPerApp = historyVulns
28 | .slice(0, lastApp)
29 | .map((entry) => timeDelta(entry.patched, entry.appeared));
30 |
31 | const finalVeloPerApp = [...nowVelocityPerApp, ...historyVelocityPerApp];
32 | const sum = finalVeloPerApp.reduce((a, b) => a + b, 0);
33 | const avg = Math.round(sum / finalVeloPerApp.length || 0);
34 |
35 | return avg;
36 | };
37 |
38 | const velocityIsHigh = (velocity, benchmark) => velocity > benchmark * 3.2;
39 | const velocityIsMedium = (velocity, benchmark) => velocity > benchmark;
40 |
41 | const vulnsIsHigh = (vulns, avg) => vulns > avg * 1.5;
42 | const vulnsIsMedium = (vulns, avg) => vulns > 0;
43 |
44 | const measureOverallRisk = (
45 | currentVulns,
46 | last30dVulns,
47 | patchVelocity,
48 | benchmarkVelocity = 24 * 60 * 60
49 | ) => {
50 | if (
51 | velocityIsHigh(patchVelocity, benchmarkVelocity) ||
52 | vulnsIsHigh(currentVulns, last30dVulns)
53 | ) {
54 | return 'high';
55 | }
56 |
57 | if (
58 | velocityIsMedium(patchVelocity, benchmarkVelocity) ||
59 | vulnsIsMedium(currentVulns, last30dVulns)
60 | ) {
61 | return 'medium';
62 | }
63 |
64 | return 'low';
65 | };
66 |
67 | const measureQuantityRisk = (currentVulns, last30dVulns) => {
68 | if (vulnsIsHigh(currentVulns, last30dVulns)) {
69 | return 'high';
70 | }
71 |
72 | if (vulnsIsMedium(currentVulns, last30dVulns)) {
73 | return 'medium';
74 | }
75 |
76 | return 'low';
77 | };
78 |
79 | const measureVelocityRisk = (
80 | patchVelocity,
81 | benchmarkVelocity = 24 * 60 * 60
82 | ) => {
83 | if (velocityIsHigh(patchVelocity, benchmarkVelocity)) {
84 | return 'high';
85 | }
86 |
87 | if (velocityIsMedium(patchVelocity, benchmarkVelocity)) {
88 | return 'medium';
89 | }
90 |
91 | return 'low';
92 | };
93 |
94 | /**
95 | * Recalculates current patching stats: risk, # of current vulns, avg. # of vulns (30d) and patch
96 | * velocity (in minutes).
97 | *
98 | * @param {*} state - an state of analyticalSlice.
99 | * @returns {Object} returns a dictionary with four elements: risk (str),
100 | * # of current vulns (integer), avg. # of vulns (30d; integer) and patch velocity (in minutes;
101 | * integer).
102 | */
103 | export default function refreshStats(state) {
104 | const currentVulns = Object.keys(state.localAffectedAppsForAnalytics).length;
105 | const last30dVulns = 5;
106 | const patchVelocity = measurePatchVelocity(
107 | state.localAffectedAppsForAnalytics,
108 | state.updateHistory
109 | );
110 | const overallRisk = measureOverallRisk(
111 | currentVulns,
112 | last30dVulns,
113 | patchVelocity
114 | );
115 | const velocityRisk = measureVelocityRisk(patchVelocity);
116 | const quantityRisk = measureQuantityRisk(currentVulns, last30dVulns);
117 | return {
118 | overallRisk,
119 | velocityRisk,
120 | quantityRisk,
121 | currentVulns,
122 | last30dVulns,
123 | patchVelocity,
124 | };
125 | }
126 |
--------------------------------------------------------------------------------
/src/features/analyticsSlice/utils/refreshStats.measurePatchVelocity.spec.js:
--------------------------------------------------------------------------------
1 | import nowUnixTime from '../../appsVulnerabilities/nowUnixTime';
2 | import { measurePatchVelocity } from './refreshStats';
3 |
4 | test('app affected 30 days ago equals to 30d avg velocity', () => {
5 | const nowUtc = nowUnixTime();
6 | const ago30days = nowUtc - 30 * 24 * 60 * 60;
7 | const nowVulns = {
8 | 'a:ff:ff': { appeared: ago30days },
9 | };
10 | expect(measurePatchVelocity(nowVulns, [], nowUtc)).toEqual(30 * 24 * 60 * 60);
11 | });
12 |
13 | test('two apps affected 2 and 4 mins ago equals to 3m avg velocity', () => {
14 | const nowUtc = nowUnixTime();
15 | const ago2mins = nowUtc - 2 * 60;
16 | const ago4mins = nowUtc - 4 * 60;
17 | const nowVulns = {
18 | 'a:ff:ff': { appeared: ago2mins },
19 | 'a:google:chrome': { appeared: ago4mins },
20 | };
21 | expect(measurePatchVelocity(nowVulns, [], nowUtc)).toEqual(3 * 60);
22 | });
23 |
24 | test('past app patched in 1 day equals to 1d avg velocity', () => {
25 | const nowUtc = nowUnixTime();
26 | const ago2days = nowUtc - 2 * 24 * 60 * 60;
27 | const ago1day = nowUtc - 1 * 24 * 60 * 60;
28 | const historyVulns = [{ appeared: ago2days, patched: ago1day }];
29 | expect(measurePatchVelocity({}, historyVulns, nowUtc)).toEqual(24 * 60 * 60);
30 | });
31 |
32 | test('past apps patched in 3 and 1 days equals to 2d avg velocity', () => {
33 | const nowUtc = nowUnixTime();
34 | const ago4days = nowUtc - 4 * 24 * 60 * 60;
35 | const ago2days = nowUtc - 2 * 24 * 60 * 60;
36 | const ago1day = nowUtc - 1 * 24 * 60 * 60;
37 | const historyVulns = [
38 | { appeared: ago4days, patched: ago1day },
39 | { appeared: ago2days, patched: ago1day },
40 | ];
41 | expect(measurePatchVelocity({}, historyVulns, nowUtc)).toEqual(2 * 24 * 60 * 60);
42 | });
43 |
44 | test('only apps patched within last 30 days count', () => {
45 | const nowUtc = nowUnixTime();
46 | const ago33days = nowUtc - 33 * 24 * 60 * 60;
47 | const ago31days = nowUtc - 31 * 24 * 60 * 60;
48 | const ago2days = nowUtc - 2 * 24 * 60 * 60;
49 | const ago1day = nowUtc - 1 * 24 * 60 * 60;
50 | const historyVulns = [
51 | { appeared: ago2days, patched: ago1day },
52 | { appeared: ago33days, patched: ago31days },
53 | ];
54 | expect(measurePatchVelocity({}, historyVulns, nowUtc)).toEqual(24 * 60 * 60);
55 | });
56 |
57 | test(`past app patched within 6 minutes and an active app flowed 4 mins ago
58 | equals to 5m avg velocity`, () => {
59 | expect(1).toBeTruthy();
60 | const nowUtc = nowUnixTime();
61 |
62 | // App appeared 4 minutes ago.
63 | const ago4mins = nowUtc - 4 * 60;
64 | const nowVulns = {
65 | 'a:google:chrome': { appeared: ago4mins },
66 | };
67 |
68 | // Another app was patched within 6 minutes.
69 | const ago10mins = nowUtc - 10 * 60;
70 | const historyVulns = [{ appeared: ago10mins, patched: ago4mins }];
71 |
72 | // Results in 5m of average velocity.
73 | expect(measurePatchVelocity(nowVulns, historyVulns, nowUtc)).toEqual(5 * 60);
74 | });
75 |
--------------------------------------------------------------------------------
/src/features/analyticsSlice/utils/refreshStats.spec.js:
--------------------------------------------------------------------------------
1 | import { ANALYTICS_STATE_SEED } from '../../../configs';
2 | import refreshStats from './refreshStats';
3 |
4 | test('should return zeros for no apps', () => {
5 | const state = ANALYTICS_STATE_SEED;
6 | const {
7 | overallRisk,
8 | velocityRisk,
9 | quantityRisk,
10 | currentVulns,
11 | last30dVulns,
12 | patchVelocity,
13 | } = refreshStats(state);
14 | expect(overallRisk).toEqual('low');
15 | expect(velocityRisk).toEqual('low');
16 | expect(quantityRisk).toEqual('low');
17 | expect(currentVulns).toEqual(0);
18 | // TODO Revert to zero, when 30d avg is calculated correctly.
19 | expect(last30dVulns).toEqual(5);
20 | expect(patchVelocity).toEqual(0);
21 | });
22 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/appVersionsAffectedBy.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param {string} cve_name CVE identifier of a vuln, i.e. "CVE-2077-0001".
4 | * @param {string} app_cpe CPE identifier of an app, i.e. "a:mozilla:firefox".
5 | * @param {Dict>} vulnsRepo
6 | * @returns
7 | */
8 | export default function appVersionsAffectedBy(cve_name, app_cpe, vulnsRepo) {
9 | return vulnsRepo[cve_name].versions.filter((i) => i.cpe === app_cpe)[0];
10 | }
11 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/appsVulnsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | import {
4 | hasPersistentState,
5 | loadPersistentState,
6 | } from '../../app/persistAppsVulnsMiddleware';
7 | import { STATE_STORAGE_APPSVULNS_KEY } from '../../configs';
8 | import { resetKey, setKey } from '../subscriptionSlice/subscriptionSlice';
9 |
10 | export const makeStateSeed = () => {
11 | return {
12 | appsRepo: {},
13 | vulnsRepo: {},
14 | repoColdSyncTime: 0, // UTC Unix time
15 | repoColdHash: '', // Used both for backend sync and local storage.
16 | repoHotHash: '', // Used both for backend sync and local storage.
17 |
18 | localApps: [],
19 | localVulnerableApps: [],
20 | localAppsHash: '', // Used for local storage.
21 | localVulnerableAppsHash: '', // Used for local storage.
22 | };
23 | };
24 |
25 | /**
26 | * Load initial state for local apps' vulns. Prefers a load from the cache. If the cache is missing,
27 | * loads state from the given seed state.
28 | *
29 | * @param {Dict} seed empty state for apps' vulns.
30 | * @param {bool} resetCachedState a flag to reset a cached state.
31 | * @returns a state for local apps' vulns.
32 | */
33 | const initState = (seed, resetCachedState = false) => {
34 | // Try to restore a state from a cache.
35 | let recoveredState = seed;
36 | if (!resetCachedState && hasPersistentState(STATE_STORAGE_APPSVULNS_KEY)) {
37 | recoveredState = {
38 | ...recoveredState,
39 | ...loadPersistentState(STATE_STORAGE_APPSVULNS_KEY),
40 | };
41 | }
42 |
43 | return recoveredState;
44 | };
45 |
46 | const appsVulnsSlice = createSlice({
47 | name: 'appsVulns',
48 | initialState: initState(makeStateSeed()),
49 | reducers: {
50 | setRepoAndAppsVulns(state, { payload }) {
51 | state.vulnsRepo = payload.vulnsRepo;
52 | state.appsRepo = payload.appsRepo;
53 | state.repoColdHash = payload.repoColdHash;
54 | state.repoHotHash = payload.repoHotHash;
55 | state.repoColdSyncTime = payload.repoColdSyncTime;
56 |
57 | state.localApps = payload.localApps;
58 | state.localVulnerableApps = payload.localVulnerableApps;
59 | state.localAppsHash = payload.localAppsHash;
60 | state.localVulnerableAppsHash = payload.localVulnerableAppsHash;
61 | },
62 |
63 | resetRepos(state, action) {
64 | console.log('reset repo hashes and sync time');
65 | state.repoColdHash = '';
66 | state.repoHotHash = '';
67 | state.repoColdSyncTime = 0;
68 | },
69 | },
70 | extraReducers: (builder) => {
71 | // When an access key changes, our repo of remote apps/vulns changes. If on the next sync round
72 | // we will download an incremental repo update, UI logic may not find necessary resources and
73 | // app will crash softly. So in order to properly resetup it, we should clear repos' hashes.
74 | builder.addCase(resetKey, (state, action) => {
75 | appsVulnsSlice.caseReducers.resetRepos(state, action);
76 | });
77 | builder.addCase(setKey, (state, action) => {
78 | appsVulnsSlice.caseReducers.resetRepos(state, action);
79 | });
80 | },
81 | });
82 |
83 | export const { setRepoAndAppsVulns } = appsVulnsSlice.actions;
84 | export default appsVulnsSlice.reducer;
85 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/coldRepoExpired.js:
--------------------------------------------------------------------------------
1 | import nowUnixTime from './nowUnixTime';
2 |
3 | const COLD_REPO_TTL = 24 * 60 * 60; // 24 hours in seconds
4 | // const COLD_REPO_TTL = 1; // 1 second in seconds
5 |
6 | export default function coldRepoExpired(lastFetchOfColdRepo) {
7 | const nowUtc = nowUnixTime();
8 | return nowUtc - lastFetchOfColdRepo > COLD_REPO_TTL;
9 | }
10 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/convertToCPEName.js:
--------------------------------------------------------------------------------
1 | import makeAppRepoAliases from './makeAppRepoAliases';
2 |
3 | /**
4 | * Converts osquery app's names to a CPE name. E.g. "Firefox" will be converted to
5 | * "mozilla:firefox".
6 | *
7 | * @param {Dictionary} osqApp a host app from osquery.
8 | * @param {Dictionary>} manaAppsRepo list of supported apps.
9 | * @returns string with CPE-like name. If the app is not supported, returns null.
10 | */
11 | export default function convertToCPEName(osqApp, manaAppsRepo) {
12 | // Make a set from osquery app's names.
13 | const osqAppAliases = new Set(osqApp.aliases);
14 |
15 | // Drop keys from mana apps' repo dictionary.
16 | const manaAppsList = Object.values(manaAppsRepo);
17 |
18 | // Find an appropriate pair for osquery app among apps supported by Mana.
19 | const foundEntry = manaAppsList.find((manaApp) => {
20 | // First, construct a set from mana app names.
21 | const manaAppAliases = new Set(makeAppRepoAliases(manaApp));
22 |
23 | // And check if there any intersections among mana app's names and osquery's ones.
24 | return (
25 | new Set([...manaAppAliases].filter((i) => osqAppAliases.has(i))).size > 0
26 | );
27 | });
28 |
29 | if (foundEntry) {
30 | return `${foundEntry.cpe_part}:${foundEntry.cpe_vendor}:${foundEntry.cpe_product}`;
31 | }
32 | return null;
33 | }
34 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/cutAppExtension.js:
--------------------------------------------------------------------------------
1 | export default function cutAppExtension(appName) {
2 | return appName && appName.length && appName.endsWith('.app')
3 | ? appName.slice(0, -4)
4 | : appName;
5 | }
6 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/detectAffectedApps.js:
--------------------------------------------------------------------------------
1 | import { loadApps, loadOS } from '../../utils/osquery/osqueryi';
2 | import getAffectedHostApps from './getAffectedHostApps';
3 | import { toOsqueryApp } from './types/osqueryApp';
4 | import { toOsqueryOS } from './types/osqueryOS';
5 |
6 | /**
7 | * Detect affected apps on a host. It makes a few things to accomplish this:
8 | * 1. Loads current host apps.
9 | * 2. Loads OS information.
10 | * 3. Detects vulns for given apps and OS.
11 | *
12 | * @returns JSON with initialized reducers.
13 | */
14 | export default function detectAffectedApps({ appsRepo, vulnsRepo, localApps }) {
15 | // const osOsquery = toOsqueryOS(rawHostOS);
16 | // const hostAppsList = rawHostApps.map((x) => toOsqueryApp(x));
17 | // const hostAppsListWithOS = hostAppsList.concat(osOsquery);
18 |
19 | const affectedHostApps = getAffectedHostApps(localApps, appsRepo, vulnsRepo);
20 |
21 | // TODO: Store local apps as : pairs.
22 | return affectedHostApps;
23 | }
24 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/dumbVulnRepository.js:
--------------------------------------------------------------------------------
1 | const dumbVulnsRepo = {
2 | 'CVE-2021-0001': {
3 | cve: 'CVE-2021-0001',
4 | description:
5 | 'Mozilla developers and community members reported memory safety bugs present in Firefox 90. Some of these bugs showed evidence of memory corruption and we presume that with enough effort some of these could have been exploited to run arbitrary code. This vulnerability affects Firefox < 91.',
6 | references: ['https://www.mozilla.org/security/advisories/mfsa2021-33/'],
7 | solution: 'Update to Mozilla version 91 or later',
8 | pub_date: '08/17/2021',
9 | cvss3_value: 0,
10 | versions: [
11 | {
12 | cpe: 'a:mozilla:firefox',
13 | vulnerable: true,
14 | last_version: '142.0.3',
15 | operator: '<',
16 | },
17 | ],
18 | },
19 | };
20 |
21 | const dumbAppsRepo = {
22 | 'a:mozilla:firefox': {
23 | // "id": "abcdef123",
24 | app_name: 'Firefox',
25 | cpe_part: 'a',
26 | cpe_vendor: 'mozilla',
27 | cpe_product: 'firefox',
28 | aliases: ['firefox', 'firefox hd'],
29 | // "homepage": "https://firefox.com/",
30 | description: 'Top web browser',
31 | vulns: ['CVE-2021-0001'],
32 | }
33 | };
34 |
35 | export { dumbVulnsRepo, dumbAppsRepo };
36 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/filterAffectedHostApps.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Filter apps with relevant vulns.
3 | *
4 | * @param {*} localAppsDict dictionary with local apps as a pair :.
5 | * @returns a dictionary of all local apps, that contain vulns.
6 | */
7 | export default function filterAffectedHostApps(localAppsDict) {
8 | return Object.fromEntries(
9 | Object.entries(localAppsDict).filter(
10 | ([k, v]) => Object.keys(v.vulns).length !== 0
11 | )
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/findVulnsFor.js:
--------------------------------------------------------------------------------
1 | import isRelevantVuln from './isRelevantVuln';
2 |
3 | export const normaliseDepth = (depthLiteral) => {
4 | switch (depthLiteral) {
5 | case 'major_minor_patch':
6 | return 3;
7 | case 'major_minor':
8 | return 2;
9 | case 'major':
10 | return 1;
11 | default:
12 | return 3;
13 | }
14 | };
15 |
16 | /**
17 | * Finds vulns from repo, that affect current version of a given local app.
18 | *
19 | * @param {str} osqAppCPEName local app's CPE.
20 | * @param {Dict} osqApp local app's object from Osquery.
21 | * @param {Dict} vulnsRepo : pairs with all repo vulns.
22 | * @param {Dict} appsRepo : pairs with all repo apps.
23 | * @returns sorted list of CVEs, that affect the given local app. Sorting is done by affected
24 | * versions: fresh versions appear before older ones.
25 | */
26 | export default function findVulnsFor(
27 | osqAppCPEName,
28 | osqApp,
29 | vulnsRepo,
30 | appsRepo
31 | ) {
32 | try {
33 | // Get all vulns for the given app.
34 | const allAppVulns = appsRepo[osqAppCPEName].vulns;
35 | const matchDepth = normaliseDepth(
36 | appsRepo[osqAppCPEName].how_to_check_versions
37 | );
38 |
39 | // Filter vulns, that affect local app.
40 | let relevantVulns = allAppVulns.filter((cveName) =>
41 | isRelevantVuln(cveName, osqAppCPEName, osqApp, vulnsRepo, matchDepth)
42 | );
43 |
44 | return relevantVulns;
45 | } catch (error) {
46 | return [];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/getAffectedHostApps.js:
--------------------------------------------------------------------------------
1 | import convertToCPEName from './convertToCPEName';
2 | import filterAffectedHostApps from './filterAffectedHostApps';
3 | import filterByNotMatchingName from '../../utils/osquery/filterByNotMatchingName';
4 | import makeAppRepoNamesSet from './makeAppRepoNamesSet';
5 | import mixinVulns from './mixinVulns';
6 |
7 | /**
8 | * Makes a list with local apps affected by vulns.
9 | *
10 | * @param {List} hostAppsList local apps' list.
11 | * @param {Dict} appsRepo supported apps as :.
12 | * @param {Dict} vulnsRepo supported vulns as :.
13 | * @returns a dict : with affected local apps and relevant vulns.
14 | */
15 |
16 | export default function getAffectedHostApps(hostAppsList, appsRepo, vulnsRepo) {
17 | // Filter apps from osquery list which Mana doesn't support.
18 | const appRepoNamesSet = makeAppRepoNamesSet(appsRepo);
19 | const hostAppsTruncated = filterByNotMatchingName(
20 | hostAppsList,
21 | appRepoNamesSet
22 | );
23 |
24 | // Normalise host apps names to a map with : pairs.
25 | const normalisedAppNames = new Map(
26 | hostAppsTruncated.map((i) => [convertToCPEName(i, appsRepo), i])
27 | );
28 |
29 | // Match apps wth vulns.
30 | const hostAppsWithVulns = mixinVulns(normalisedAppNames, vulnsRepo, appsRepo);
31 | const affectedHostApps = filterAffectedHostApps(hostAppsWithVulns);
32 |
33 | return affectedHostApps;
34 | }
35 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/isRelevantVuln.js:
--------------------------------------------------------------------------------
1 | import appVersionsAffectedBy from './appVersionsAffectedBy';
2 | import versionBelongs from './versionBelongs';
3 |
4 | /**
5 | * Checks if a given app is affected by the vulnerability.
6 | *
7 | * @param {string} cveName CVE identifier of a vuln, i.e. "CVE-2077-0001".
8 | * @param {string} appCPEName CPE identifier of an app, i.e. "a:mozilla:firefox".
9 | * @param {Any} appObj app object from Osquery.
10 | * @param {List} vulns all supported vulns.
11 | * @returns true if a given app version is affected by this vulnerability. Otherwise – false.
12 | */
13 | export default function isRelevantVuln(
14 | cveName,
15 | appCPEName,
16 | appObj,
17 | vulns,
18 | depth
19 | ) {
20 | // Versions affected by a vuln can contain several affected apps. We have to drop irrelevant
21 | // apps. Also, there might be several
22 | const vulnVersion = appVersionsAffectedBy(cveName, appCPEName, vulns);
23 |
24 | // Compare affected version with local version.
25 | if (vulnVersion) {
26 | return versionBelongs(
27 | appObj.currentVersion,
28 | vulnVersion.last_version,
29 | vulnVersion.operator,
30 | depth,
31 | appObj
32 | );
33 | }
34 |
35 | return false;
36 | }
37 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/makeAppRepoAliases.js:
--------------------------------------------------------------------------------
1 | import cutAppExtension from "./cutAppExtension";
2 |
3 | /**
4 | * Generates all possible names for a given Mana app. Mana app contains name variant in several
5 | * fields: "app_name" and "aliases". Both fields first lowercased and then returned as a list.
6 | *
7 | * Sidenote: "aliases" field contains a string with all known name variants. Each variant is
8 | * separated with a comma.
9 | *
10 | * @param {Dict} repoApp supported Mana app.
11 | * @returns list with Mana app name's aliases.
12 | */
13 | export default function makeAppRepoAliases(repoApp) {
14 | // Mana analysts put app name into "osquery_appname" field.
15 | const osqAppName = repoApp.os_query_app_name
16 | ? cutAppExtension(repoApp.os_query_app_name)
17 | : '';
18 |
19 | // App name alternatives are stored in 'aliases' field.
20 | const aliases = repoApp.aliases.concat([osqAppName]);
21 |
22 | // App aliases should be lowercased and without empty/null elements.
23 | const aliasesWithoutEmptyStrings = aliases.filter((x) => x.length > 0);
24 | const lowercaseAliases = aliasesWithoutEmptyStrings.map((x) =>
25 | x.toLowerCase()
26 | );
27 |
28 | // Remove duplicates
29 | const uniqueAliases = lowercaseAliases.filter(
30 | (elem, index, self) => index === self.indexOf(elem)
31 | );
32 |
33 | return uniqueAliases;
34 | }
35 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/makeAppRepoNamesSet.js:
--------------------------------------------------------------------------------
1 | import makeAppRepoAliases from './makeAppRepoAliases';
2 |
3 | /**
4 | * Composes a set with all supported apps' names. Includes both plain app name and its aliases.
5 | *
6 | * @param {list} appRepo a list of all supported apps.
7 | * @returns A set with all repo apps' names. All names are lowercase.
8 | */
9 | export default function makeAppRepoNamesSet(appRepo) {
10 | // The app repository is a dictionary like :. We only need the full
11 | // object, so let's drop dictionary keys.
12 | const appRepoVals = Object.values(appRepo);
13 |
14 | return appRepoVals.reduce((appSet, val) => {
15 | const nameAliases = makeAppRepoAliases(val);
16 | nameAliases.forEach((element) => {
17 | appSet.add(element.toLowerCase());
18 | });
19 |
20 | return appSet;
21 | }, new Set());
22 | }
23 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/mixinVulns.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 | import appVersionsAffectedBy from './appVersionsAffectedBy';
3 | import findVulnsFor, { normaliseDepth } from './findVulnsFor';
4 | import versionBelongs from './versionBelongs';
5 |
6 | export const UNRECOGNISED_VULN_DESCRIPTION = `
7 | This app frequently contains security patches. It is strongly recommended to update it while
8 | our researchers assess the security impact.
9 | `;
10 |
11 | const getAffectedVersionsFor = (vulnCVE, appCPE, vulnsRepo) => {
12 | const vulnVersion = appVersionsAffectedBy(vulnCVE, appCPE, vulnsRepo);
13 | const vulnVersionAndDescr = {
14 | ...vulnVersion,
15 | description: vulnsRepo[vulnCVE].description,
16 | };
17 |
18 | return [vulnCVE, vulnVersionAndDescr];
19 | };
20 |
21 | export function getForcedUpdateVuln(localAppCPE, localApp, remoteApp) {
22 | const localVer = localApp.currentVersion;
23 | let remoteVersions = [];
24 | if (remoteApp.app_versions.length) {
25 | const LTSCandidate = remoteApp.app_versions[0];
26 | const LTSRemoteVersion = LTSCandidate.current_version;
27 |
28 | // If current version <= LTS version.
29 | if (
30 | /\d/.test(LTSRemoteVersion) &&
31 | versionBelongs(localVer, LTSRemoteVersion, '<=', 1, localApp)
32 | ) {
33 | // Add LTS version for comparison.
34 | remoteVersions = remoteVersions.concat([remoteApp.app_versions[0]]);
35 | }
36 | }
37 |
38 | if (!remoteVersions.length) {
39 | remoteVersions = remoteVersions.concat([remoteApp]);
40 | }
41 |
42 | const finalForcedVuln = remoteVersions.map((remoteAppCandidate) => {
43 | let forcedVuln = false;
44 |
45 | const remoteVer = remoteAppCandidate.current_version;
46 | const matchDepth = normaliseDepth(remoteAppCandidate.how_to_check_versions);
47 | if (
48 | /\d/.test(remoteVer) &&
49 | versionBelongs(localVer, remoteVer, '<', matchDepth, localApp)
50 | ) {
51 | forcedVuln = [
52 | `MANA-${localAppCPE.toUpperCase()}-${remoteVer}`,
53 | {
54 | operator: '<',
55 | last_version: remoteVer,
56 | description: UNRECOGNISED_VULN_DESCRIPTION,
57 | },
58 | ];
59 | }
60 |
61 | return forcedVuln;
62 | });
63 | const finalForcedVulnNonNull = finalForcedVuln.filter((el) => el);
64 |
65 | return finalForcedVulnNonNull.length ? finalForcedVulnNonNull[0] : false;
66 | }
67 |
68 | /**
69 | * Add relevant vulns for each local app into "vulns" field. Only adds vulns, that current version
70 | * of the app is affected by.
71 | *
72 | * @param {*} localAppsDict dictionary with local apps as a pair :.
73 | * @param {*} vulnsRepo list of all supported vulns.
74 | * @param {*} appsRepo list of all supported apps.
75 | * @returns a dictionary of all local apps with detected vulns.
76 | */
77 | export default function mixinVulns(localAppsDict, vulnsRepo, appsRepo) {
78 | return Object.fromEntries(
79 | localAppsDict.entries().map(([localAppCPE, localApp]) => {
80 | // Find applicable vulns.
81 | // HACK for the 1st version: we support two last macOS versions and do not have ternary
82 | // operators, so we can't properly match vulns for LTS version of macOS.
83 | let vulnsWithVersions = {};
84 | if (localAppCPE !== 'o:apple:macos') {
85 | const vulns = findVulnsFor(localAppCPE, localApp, vulnsRepo, appsRepo);
86 |
87 | // Add affected versions info for each vuln.
88 | vulnsWithVersions = Object.fromEntries(
89 | vulns.map((vulnCVE) =>
90 | getAffectedVersionsFor(vulnCVE, localAppCPE, vulnsRepo)
91 | )
92 | );
93 | }
94 |
95 | // Create a simulated update for apps, that require last version installed.
96 | const forcedUpdate = getForcedUpdateVuln(
97 | localAppCPE,
98 | localApp,
99 | appsRepo[localAppCPE]
100 | );
101 |
102 | // Append last app version to the beginning, if app should be updated unconditionally.
103 | if (
104 | appsRepo[localAppCPE].update_anyway &&
105 | Object.keys(vulnsWithVersions).length === 0 &&
106 | forcedUpdate
107 | ) {
108 | const forcedUpdateId = forcedUpdate[0];
109 | const forcedUpdateDescription = forcedUpdate[1];
110 | vulnsWithVersions[forcedUpdateId] = forcedUpdateDescription;
111 | }
112 |
113 | // Save vulns with affected versions to local app meta.
114 | const localAppWithVulns = produce(localApp, (draft) => {
115 | draft.vulns = vulnsWithVersions;
116 | });
117 |
118 | return [localAppCPE, localAppWithVulns];
119 | })
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/nowUnixTime.js:
--------------------------------------------------------------------------------
1 | export default function nowUnixTime() {
2 | return Math.floor(new Date().getTime() / 1000);
3 | }
4 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/service/vulnApi.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2 | import filterRelevantCVENames from '../../../utils/nvd/filterRelevantCVENames';
3 | import getCPEName from '../../../utils/nvd/getCPEName';
4 | import { API_HOST } from '../../../configs';
5 |
6 | export const API_POLLING_INTERVAL = 5 * 60 * 1000; // Every 5 minutes we call API endpoints.
7 | export const QUERY_OPTIONS = { pollingInterval: API_POLLING_INTERVAL };
8 |
9 | const getUserToken = () => {
10 | return 'c682c26fe344473e6efb6f08fedeba3af388ad92';
11 | };
12 |
13 | const transformRepository = (response) => {
14 | // Transform apps into a dict by CPE name.
15 | const appsById = response.apps.reduce((buff, app) => {
16 | // Detect a CPE name.
17 | const cpeName = getCPEName(app);
18 |
19 | // Initial apps' dict does not contain a mapping to vulns. To improve the performance
20 | // of vulnerability lookup, we have to add all relevant CVE ids into the app.
21 | const vulns = filterRelevantCVENames(cpeName, response.vulns);
22 |
23 | // Now everything is ready, so we transform initial app object into a map: :.
24 | buff[cpeName] = {
25 | ...app,
26 | vulns,
27 | };
28 | return buff;
29 | }, {});
30 |
31 | // Transform vulns into a dict by CVE id.
32 | console.info(`Received ${response.vulns.length} vulns`);
33 | const vulnsById = response.vulns.reduce((buff, vuln) => {
34 | buff[vuln.cve] = vuln;
35 | return buff;
36 | }, {});
37 |
38 | return {
39 | apps: appsById,
40 | vulns: vulnsById,
41 | };
42 | };
43 |
44 | export const vulnApi = createApi({
45 | reducerPath: 'vulnApi',
46 | baseQuery: fetchBaseQuery({
47 | // fetch is not defined in test environment (SSR), so we should make a dumb mock. Works until
48 | // actual tests will execute this method. If this's the case, mock the fetch function.
49 | fetchFn: typeof fetch === 'function' ? fetch : () => '',
50 | // TODO: IMPORTANT This endpoint leaks information.
51 | baseUrl: `${API_HOST}api/v1.0/`,
52 | prepareHeaders: (headers) => {
53 | headers.set('Authorization', `Token ${getUserToken()}`);
54 | return headers;
55 | },
56 | }),
57 |
58 | endpoints: (builder) => ({
59 | /**
60 | * DEPRECATED
61 | *
62 | * Download a full vulns repository from our backend.
63 | *
64 | * @param {dict} response a dict of supported apps and vulns from the Mana backend. Each root
65 | * level field is a list with app's or vuln's object.
66 | * @returns {dict} an initial dictionary with transformed apps' and vulns' lists into a dict.
67 | * The dicts contain id and initial object value: : for an app object, :
68 | * for a vuln object. Also, the result object contains a list with relevant CVE ids.
69 | */
70 | getFullVulns: builder.query({
71 | query: () => `assets/vuln_bckps/all_vulns/`,
72 | transformResponse: (response) => transformRepository(response),
73 | }),
74 |
75 | /**
76 | * DEPRECATED
77 | *
78 | * Same as getFullVulns, but downloads a repository with recent changes.
79 | */
80 | getRecentVulns: builder.query({
81 | query: () => `assets/vuln_bckps/new_vulns/`,
82 | transformResponse: (response) => transformRepository(response),
83 | }),
84 |
85 | /**
86 | * DEPRECATED
87 | *
88 | * Fetches hashes of Mana datasets.
89 | */
90 | getMetas: builder.query({
91 | query: () => `assets/vuln_bckps/`,
92 | transformResponse: (response) => {
93 | const metasByName = response.results.reduce((buff, repoMeta) => {
94 | buff[repoMeta.name] = repoMeta;
95 | return buff;
96 | }, {});
97 |
98 | return metasByName;
99 | },
100 | }),
101 | }),
102 | });
103 |
104 | // Export hooks for usage in functional components, which are
105 | // auto-generated based on the defined endpoints
106 | // eslint-disable-next-line prettier/prettier
107 | export const { useGetFullVulnsQuery, useGetMetasQuery, useGetRecentVulnsQuery } = vulnApi;
108 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/tests/convertToCPEName.spec.js:
--------------------------------------------------------------------------------
1 | const { default: convertToCPEName } = require('../convertToCPEName');
2 | const { OsqueryApp } = require('../types/osqueryApp');
3 |
4 | test('firefox should be translated into "mozilla:firefox"', () => {
5 | const ffOsqApp = new OsqueryApp({
6 | name: 'Firefox',
7 | bundle_name: 'Mozilla Firefox',
8 | });
9 | const ffManaApp = {
10 | cpe_part: 'a',
11 | cpe_vendor: 'mozilla',
12 | cpe_product: 'firefox',
13 | os_query_app_name: 'Firefox',
14 | aliases: ['Mozilla Firefox', 'Mozilla Firefox ESR'],
15 | };
16 | expect(
17 | convertToCPEName(ffOsqApp, { 'a:mozilla:firefox': ffManaApp })
18 | ).toEqual('a:mozilla:firefox');
19 | });
20 |
21 | test('chrome should be translated into "google:chrome" given two supported apps', () => {
22 | const chromeOsqApp = new OsqueryApp({
23 | name: 'Google Chrome.app',
24 | bundle_name: 'Chrome',
25 | });
26 |
27 | const ffManaApp = {
28 | cpe_part: 'a',
29 | cpe_vendor: 'mozilla',
30 | cpe_product: 'firefox',
31 | os_query_app_name: 'Firefox',
32 | aliases: ['Mozilla Firefox', 'Mozilla Firefox ESR'],
33 | };
34 |
35 | const chromeManaApp = {
36 | cpe_part: 'a',
37 | cpe_vendor: 'google',
38 | cpe_product: 'chrome',
39 | os_query_app_name: 'chrome',
40 | aliases: ['Google Chrome'],
41 | };
42 |
43 | const manaAppRepo = {
44 | 'a:mozilla:firefox': ffManaApp,
45 | 'a:google:chrome': chromeManaApp,
46 | };
47 |
48 | expect(convertToCPEName(chromeOsqApp, manaAppRepo)).toEqual(
49 | 'a:google:chrome'
50 | );
51 | });
52 |
53 | test('not supported app should be translated into null', () => {
54 | const ffOsqApp = new OsqueryApp({
55 | name: 'Firefox',
56 | bundle_name: 'Mozilla Firefox',
57 | });
58 |
59 | const chromeManaApp = {
60 | cpe_part: 'a',
61 | cpe_vendor: 'google',
62 | cpe_product: 'chrome',
63 | os_query_app_name: 'Google Chrome',
64 | aliases: ['chrome'],
65 | };
66 |
67 | expect(
68 | convertToCPEName(ffOsqApp, { 'a:google:chrome': chromeManaApp })
69 | ).toEqual(null);
70 | });
71 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/tests/forcedVuln.spec.js:
--------------------------------------------------------------------------------
1 | import {
2 | getForcedUpdateVuln,
3 | UNRECOGNISED_VULN_DESCRIPTION,
4 | } from '../mixinVulns';
5 |
6 | test('app with version 1.2 matches with < 1.3', () => {
7 | const localApp = {
8 | currentVersion: '1.2',
9 | };
10 |
11 | const remoteApp = {
12 | current_version: '1.3',
13 | how_to_check_versions: 'major_minor_patch',
14 | update_anyway: true,
15 | app_versions: [],
16 | };
17 |
18 | const forcedVuln = getForcedUpdateVuln(
19 | 'a:mozilla:firefox',
20 | localApp,
21 | remoteApp
22 | );
23 | const expectedResult = {
24 | operator: '<',
25 | last_version: '1.3',
26 | description: UNRECOGNISED_VULN_DESCRIPTION,
27 | };
28 | expect(forcedVuln).toEqual(['MANA-A:MOZILLA:FIREFOX-1.3', expectedResult]);
29 | });
30 |
31 | test('app with version 1.3 does not matches with < 1.3', () => {
32 | const localApp = {
33 | currentVersion: '1.3',
34 | };
35 |
36 | const remoteApp = {
37 | current_version: '1.3',
38 | how_to_check_versions: 'major_minor_patch',
39 | update_anyway: true,
40 | app_versions: [],
41 | };
42 |
43 | const forcedVuln = getForcedUpdateVuln(
44 | 'a:mozilla:firefox',
45 | localApp,
46 | remoteApp
47 | );
48 | expect(forcedVuln).toBeFalsy();
49 | });
50 |
51 | test('for app with version 1.2 does not matches with range [<1.2, <2.7]', () => {
52 | const localApp = {
53 | currentVersion: '1.2',
54 | };
55 |
56 | const remoteApp = {
57 | current_version: '2.7',
58 | how_to_check_versions: 'major_minor_patch',
59 | update_anyway: true,
60 | app_versions: [
61 | {
62 | current_version: '1.2',
63 | how_to_check_versions: 'major_minor_patch',
64 | },
65 | ],
66 | };
67 |
68 | const forcedVuln = getForcedUpdateVuln(
69 | 'a:mozilla:firefox',
70 | localApp,
71 | remoteApp
72 | );
73 | expect(forcedVuln).toBeFalsy();
74 | });
75 |
76 | test('for app with version 1.2 matches with range [<1.3, <2.7]', () => {
77 | const localApp = {
78 | currentVersion: '1.2',
79 | };
80 |
81 | const remoteApp = {
82 | current_version: '2.7',
83 | how_to_check_versions: 'major_minor_patch',
84 | update_anyway: true,
85 | app_versions: [
86 | {
87 | current_version: '1.3',
88 | how_to_check_versions: 'major_minor_patch',
89 | },
90 | ],
91 | };
92 |
93 | const forcedVuln = getForcedUpdateVuln(
94 | 'a:mozilla:firefox',
95 | localApp,
96 | remoteApp
97 | );
98 | const expectedResult = {
99 | operator: '<',
100 | last_version: '1.3',
101 | description: UNRECOGNISED_VULN_DESCRIPTION,
102 | };
103 | expect(forcedVuln).toEqual(['MANA-A:MOZILLA:FIREFOX-1.3', expectedResult]);
104 | });
105 |
106 | test('for app with version 2.6 matches with range [<1.3, <2.7]', () => {
107 | const localApp = {
108 | currentVersion: '2.6',
109 | };
110 |
111 | const remoteApp = {
112 | current_version: '2.7',
113 | how_to_check_versions: 'major_minor_patch',
114 | update_anyway: true,
115 | app_versions: [
116 | {
117 | current_version: '1.2',
118 | how_to_check_versions: 'major_minor_patch',
119 | },
120 | ],
121 | };
122 |
123 | const forcedVuln = getForcedUpdateVuln(
124 | 'a:mozilla:firefox',
125 | localApp,
126 | remoteApp
127 | );
128 | const expectedResult = {
129 | operator: '<',
130 | last_version: '2.7',
131 | description: UNRECOGNISED_VULN_DESCRIPTION,
132 | };
133 | expect(forcedVuln).toEqual(['MANA-A:MOZILLA:FIREFOX-2.7', expectedResult]);
134 | });
135 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/tests/makeAppRepoAliases.spec.js:
--------------------------------------------------------------------------------
1 | import makeAppRepoAliases from '../makeAppRepoAliases';
2 |
3 | test('aliases are being extracted from "os_query_app_name" and "aliases" fields', () => {
4 | const ffManaApp = {
5 | app_name: 'Firefox 92',
6 | os_query_app_name: 'Firefox',
7 | aliases: ['Mozilla Firefox', 'Mozilla Firefox ESR'],
8 | };
9 |
10 | expect(makeAppRepoAliases(ffManaApp).length).toBe(3);
11 | expect(makeAppRepoAliases(ffManaApp).includes('firefox')).toBeTruthy();
12 | expect(makeAppRepoAliases(ffManaApp).includes('mozilla firefox')).toBeTruthy();
13 | expect(makeAppRepoAliases(ffManaApp).includes('mozilla firefox esr')).toBeTruthy();
14 | });
15 |
16 | test('aliases do not duplicate', () => {
17 | const ffManaApp = {
18 | os_query_app_name: 'Firefox',
19 | aliases: ['Firefox'],
20 | };
21 |
22 | expect(makeAppRepoAliases(ffManaApp).length).toBe(1);
23 | expect(makeAppRepoAliases(ffManaApp).includes('firefox')).toBeTruthy();
24 | });
25 |
26 | test('null "aliases" field works properly', () => {
27 | const ffManaApp = {
28 | aliases: [],
29 | os_query_app_name: null,
30 | };
31 |
32 | expect(makeAppRepoAliases(ffManaApp).length).toBe(0);
33 | });
34 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/tests/makeAppRepoNamesSet.spec.js:
--------------------------------------------------------------------------------
1 | import makeAppRepoNamesSet from "../makeAppRepoNamesSet";
2 |
3 | test('Firefox app should be reduced into a single element set with "firefox" in it', () => {
4 | const ffManaApp = {
5 | 'a:mozilla:ff': {
6 | os_query_app_name: 'Firefox',
7 | aliases: [],
8 | },
9 | };
10 | const ffSet = makeAppRepoNamesSet(ffManaApp);
11 | expect(ffSet.size).toBe(1);
12 | expect(ffSet.has('firefox')).toBeTruthy();
13 | });
14 |
15 | test('Same app names should be appear only once', () => {
16 | const ffManaApp = {
17 | 'a:mozilla:ff': {
18 | os_query_app_name: 'Firefox',
19 | aliases: ['firefox'],
20 | },
21 | };
22 | const ffSet = makeAppRepoNamesSet(ffManaApp);
23 | expect(ffSet.size).toBe(1);
24 | expect(ffSet.has('firefox')).toBeTruthy();
25 | });
26 |
27 | test('App aliases should be included into the result set', () => {
28 | const ffManaApp = {
29 | 'a:mozilla:ff': {
30 | os_query_app_name: 'Firefox',
31 | aliases: ['firefox hd', 'firefox max pro'],
32 | },
33 | };
34 | const ffSet = makeAppRepoNamesSet(ffManaApp);
35 | expect(ffSet.size).toBe(3);
36 | expect(ffSet.has('firefox')).toBeTruthy();
37 | expect(ffSet.has('firefox hd')).toBeTruthy();
38 | expect(ffSet.has('firefox max pro')).toBeTruthy();
39 | });
40 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/tests/osqueryApp.spec.js:
--------------------------------------------------------------------------------
1 | import { OsqueryApp } from '../types/osqueryApp';
2 |
3 | test('should return "name" and "bundle_name" field', () => {
4 | const ff = new OsqueryApp({
5 | name: 'Firefox',
6 | bundle_name: 'Mozilla Firefox',
7 | });
8 |
9 | expect(ff.aliases.length).toBe(2);
10 | expect(ff.aliases.includes('firefox')).toBeTruthy();
11 | expect(ff.aliases.includes('mozilla firefox')).toBeTruthy();
12 | });
13 |
14 | test('should trim ".app" ending in the "name" andfield', () => {
15 | const ff = new OsqueryApp({
16 | name: 'Firefox.app',
17 | bundle_name: 'Mozilla Firefox',
18 | });
19 |
20 | expect(ff.aliases.length).toBe(2);
21 | expect(ff.aliases.includes('firefox')).toBeTruthy();
22 | expect(ff.aliases.includes('mozilla firefox')).toBeTruthy();
23 | });
24 |
25 | test('should contain only unique app names', () => {
26 | const ff = new OsqueryApp({
27 | name: 'Firefox.app',
28 | bundle_name: 'Firefox',
29 | });
30 |
31 | expect(ff.aliases.length).toBe(1);
32 | expect(ff.aliases.includes('firefox')).toBeTruthy();
33 | });
34 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/tests/versionBelongs.spec.js:
--------------------------------------------------------------------------------
1 | import versionBelongs from '../versionBelongs';
2 |
3 | test('1.0 < 1.1 should be true', () => {
4 | expect(versionBelongs('1.0', '1.1', '<')).toBeTruthy();
5 | });
6 |
7 | test('1.1 < 1.1 should be false', () => {
8 | expect(versionBelongs('1.1', '1.1', '<')).toBeFalsy();
9 | });
10 |
11 | test('1.0 <= 1.1 should be true', () => {
12 | expect(versionBelongs('1.0', '1.1', '<=')).toBeTruthy();
13 | });
14 |
15 | test('1.1 <= 1.1 should be true', () => {
16 | expect(versionBelongs('1.1', '1.1', '<=')).toBeTruthy();
17 | });
18 |
19 | test('1.2 <= 1.1 should be false', () => {
20 | expect(versionBelongs('1.2', '1.1', '<=')).toBeFalsy();
21 | });
22 |
23 | /**
24 | * Null values or incorrect version strings (e.g. without digits) should be ok.
25 | */
26 | test('empty local version should not belong to <=1.0', () => {
27 | expect(versionBelongs(null, '1.0', '<=')).toBeFalsy();
28 | });
29 |
30 | test('local app should not belong to an empty remote version', () => {
31 | expect(versionBelongs('1.0', null, '<=')).toBeFalsy();
32 | });
33 |
34 | test('version "A.B.C" should not belong to <=1.0', () => {
35 | expect(versionBelongs('A.B.C', '1.0', '<=')).toBeFalsy();
36 | });
37 |
38 | /**
39 | * OneDrive contains leading zeros in patch version.
40 | */
41 | test('versions with leading zero in patch should be ok: 1.0.01 < 1.0.02 should be true', () => {
42 | expect(versionBelongs('1.0.01', '1.0.02', '<')).toBeTruthy();
43 | });
44 |
45 | test('versions with leading zero in patch should be ok: 1.0.02 < 1.0.02 should be false', () => {
46 | expect(versionBelongs('1.0.02', '1.0.02', '<')).toBeFalsy();
47 | });
48 |
49 | test('versions with leading zero in patch should be ok: 1.0.02 <= 1.0.02 should be true', () => {
50 | expect(versionBelongs('1.0.02', '1.0.02', '<=')).toBeTruthy();
51 | });
52 |
53 | test('versions with leading zero in patch should be ok: 1.0.03 <= 1.0.02 should be false', () => {
54 | expect(versionBelongs('1.0.03', '1.0.02', '<=')).toBeFalsy();
55 | });
56 |
57 | /**
58 | * Versions might contain a build number besides the version. E.g. Osquery once detected Zoom with
59 | * '5.8.1 (1983)' version. There're at least three variations to put a build version: '1.2.3 (456)',
60 | * '1.2.3-456' and '1.2.3.456'.
61 | *
62 | * Examples of such apps: OneDrive, Parallels Desktop and Zoom.
63 | */
64 | test('1.0.02 (1983) should belong to <=1.0.02', () => {
65 | expect(versionBelongs('1.0.02 (1983)', '1.0.02', '<=')).toBeTruthy();
66 | });
67 |
68 | test('and viceversa: 1.0.02 should belong to <=1.0.02 (1983)', () => {
69 | expect(versionBelongs('1.0.02', '1.0.02 (1983)', '<=')).toBeTruthy();
70 | });
71 |
72 | test('1.2.3-456 should belong to <=1.2.3', () => {
73 | expect(versionBelongs('1.2.3-456', '1.2.3', '<=')).toBeTruthy();
74 | });
75 |
76 | test('and viceversa: 1.2.3 should belong to <=1.2.3-456', () => {
77 | expect(versionBelongs('1.2.3', '1.2.3-456', '<=')).toBeTruthy();
78 | });
79 |
80 | test('16.29.0 should not belong to <16.29.0.57', () => {
81 | expect(versionBelongs('16.29.0', '16.29.0.57', '<')).toBeFalsy();
82 | });
83 |
84 | test('16.29.0 should belong to <16.29.1.57', () => {
85 | expect(versionBelongs('16.29.0', '16.29.1.57', '<')).toBeTruthy();
86 | });
87 |
88 | test('7.2.1.2 should belong to <=7.2.1', () => {
89 | expect(versionBelongs('7.2.1.2', '7.2.1', '<=')).toBeTruthy();
90 | });
91 |
92 | test('7.2.2.2 should not belong to <=7.2.1', () => {
93 | expect(versionBelongs('7.2.2.2', '7.2.1', '<=')).toBeFalsy();
94 | });
95 |
96 | // Compare versions limiting them to match only first two parts: major and minor. E.g.,
97 | // 'Kindle for Mac' has "1.33.0" version in osquery and backend has "1.33.62000" for the same
98 | // build.
99 | test.todo('1.33.0 should belong to 1.33.62000 when passed special parameters');
100 |
101 | test('1.2.3 with dropped patch should belong to <=1.2', () => {
102 | expect(versionBelongs('1.2.3', '1.2', '<=', 2)).toBeTruthy();
103 | });
104 |
105 | test('1.2.4 should belong to <=1.2.3 with dropped patch', () => {
106 | expect(versionBelongs('1.2.4', '1.2.3', '<=', 2)).toBeTruthy();
107 | });
108 |
109 | test('1.2 with dropped minor should belong to <=1', () => {
110 | expect(versionBelongs('1.2', '1', '<=', 1)).toBeTruthy();
111 | });
112 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/types/osqueryApp.tsx:
--------------------------------------------------------------------------------
1 | import cutAppExtension from '../cutAppExtension';
2 | import OsqueryProduct from './osqueryProduct';
3 |
4 | export class OsqueryApp extends OsqueryProduct {
5 | constructor({
6 | bundle_name = '',
7 | bundle_short_version = '',
8 | name = '',
9 | path = '',
10 | vulns = {},
11 | } = {}) {
12 | const appName = cutAppExtension(name);
13 | const nameFields = [bundle_name.toLowerCase(), appName.toLowerCase()];
14 | const aliases = nameFields.filter(
15 | (elem, index, self) => index === self.indexOf(elem)
16 | );
17 |
18 | super(bundle_short_version, aliases, path, vulns);
19 | }
20 | }
21 |
22 | export function toOsqueryApp(raw_record: Record): OsqueryApp {
23 | const osqApp = new OsqueryApp({
24 | bundle_name: raw_record.bundle_name,
25 | bundle_short_version: raw_record.bundle_short_version,
26 | name: raw_record.name,
27 | path: raw_record.path,
28 | vulns: {},
29 | });
30 |
31 | return osqApp;
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/types/osqueryOS.tsx:
--------------------------------------------------------------------------------
1 | import OsqueryProduct from './osqueryProduct';
2 |
3 | export class OsqueryOS extends OsqueryProduct {
4 | constructor(
5 | major: string,
6 | minor: string,
7 | patch: string,
8 | vulns = {},
9 | productName = 'macos'
10 | ) {
11 | const version = `${major}.${minor}.${patch}`;
12 | super(version, [productName], '/', vulns);
13 | }
14 | }
15 |
16 | export function toOsqueryOS(raw_record: Record): OsqueryOS {
17 | const osqOS = new OsqueryOS(
18 | raw_record.major,
19 | raw_record.minor,
20 | raw_record.patch
21 | );
22 |
23 | return osqOS;
24 | }
25 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/types/osqueryProduct.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 | export default class OsqueryProduct {
3 | currentVersion: string;
4 |
5 | aliases: Array;
6 |
7 | path: string;
8 |
9 | vulns;
10 |
11 | constructor(
12 | currentVersion: string,
13 | aliases: Array,
14 | path: string,
15 | vulns
16 | ) {
17 | this.currentVersion = currentVersion;
18 | this.aliases = aliases;
19 | this.path = path;
20 | this.vulns = vulns;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/features/appsVulnerabilities/versionBelongs.js:
--------------------------------------------------------------------------------
1 | import semver from 'semver';
2 |
3 | const matchDepth = (ver, depth) => {
4 | const coercedVer = semver.coerce(ver, { loose: true });
5 | const validDepth = depth >= 1 && depth <= 3 ? depth : 3;
6 | if (!semver.valid(coercedVer)) {
7 | console.error(`invalid ver=${ver}`);
8 | return ver;
9 | }
10 |
11 | const { major, minor, patch } = coercedVer;
12 |
13 | return [major, minor, patch].slice(0, validDepth).join('.');
14 | };
15 |
16 | /**
17 | * Detects if local app's version belongs to vulnerable versions.
18 | *
19 | * @param {string} localVer local app version
20 | * @param {string} affectedMaxVer max vulnerable version
21 | * @param {string} operator include or exclude current version. Might be "<" or "<="
22 | * @param {integer} depth which parts of versions to compare. Varies in a range of [1...3]. Defaults
23 | * to 3, i.e. major, minor and patch should be taken into account.
24 | * @returns true if local app is affected. Otherwise, returns false.
25 | */
26 | export default function versionBelongs(
27 | localVer,
28 | affectedMaxVer,
29 | operator,
30 | depth = 3,
31 | localApp = false
32 | ) {
33 | const localVerWithDepth = matchDepth(localVer, depth);
34 | const affectedMaxVerWithDepth = matchDepth(affectedMaxVer, depth);
35 | try {
36 | if (operator === '<') {
37 | return semver.lt(
38 | semver.coerce(localVerWithDepth, { loose: true }),
39 | semver.coerce(affectedMaxVerWithDepth, { loose: true })
40 | );
41 | }
42 | if (operator === '<=') {
43 | return semver.lte(
44 | semver.coerce(localVerWithDepth, { loose: true }),
45 | semver.coerce(affectedMaxVerWithDepth, { loose: true })
46 | );
47 | }
48 | console.error(
49 | `Unsupported versions or operator:
50 | localVer=${localVer},
51 | affectedMaxVer=${affectedMaxVer},
52 | operator=${operator}
53 | depth=${depth}`
54 | );
55 | } catch(e) {
56 | console.error(
57 | 'Failed during version processing version "%O" for app %O with error: %O',
58 | localVer,
59 | localApp,
60 | e
61 | );
62 | }
63 | return false;
64 | }
65 |
--------------------------------------------------------------------------------
/src/features/statusSlice/statusSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | import { STATUS_STATE_SEED, STATE_STORAGE_STATUS_KEY } from '../../configs';
4 | import {
5 | hasPersistentState,
6 | loadPersistentState,
7 | } from '../../app/persistAppsVulnsMiddleware';
8 |
9 | export const loadState = (seed, resetCachedState = false) => {
10 | // Try to restore a state from a cache.
11 | let recoveredState = seed;
12 | if (!resetCachedState && hasPersistentState(STATE_STORAGE_STATUS_KEY)) {
13 | recoveredState = loadPersistentState(STATE_STORAGE_STATUS_KEY);
14 | }
15 |
16 | // Refresh analytical stats after last save.
17 | const { firstLaunch } = recoveredState;
18 | return {
19 | ...seed,
20 | firstLaunch,
21 | };
22 | };
23 |
24 | const statusSlice = createSlice({
25 | name: 'appsVulns',
26 | initialState: loadState(STATUS_STATE_SEED),
27 | reducers: {
28 | setSyncStatus(state, { payload }) {
29 | const { inSync } = payload;
30 |
31 | const onboardingCompleted = state.syncInProgress && !inSync;
32 | if (onboardingCompleted) {
33 | state.firstLaunch = false;
34 | }
35 |
36 | state.syncInProgress = inSync;
37 | },
38 |
39 | setOnline(state, { isOnline }) {
40 | state.online = isOnline;
41 | },
42 | },
43 | });
44 |
45 | export const { setSyncStatus, setOnline } = statusSlice.actions;
46 | export default statusSlice.reducer;
47 |
--------------------------------------------------------------------------------
/src/features/subscriptionSlice/subscriptionSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | import {
4 | STATE_STORAGE_SUBSCRIPTION_KEY,
5 | SUBSCRIPTION_STATE_SEED,
6 | } from '../../configs';
7 | import {
8 | hasPersistentState,
9 | loadPersistentState,
10 | } from '../../app/persistAppsVulnsMiddleware';
11 |
12 | export const loadState = (seed, resetCachedState = false) => {
13 | // Try to restore a state from a cache.
14 | let recoveredState = seed;
15 | if (!resetCachedState && hasPersistentState(STATE_STORAGE_SUBSCRIPTION_KEY)) {
16 | recoveredState = loadPersistentState(STATE_STORAGE_SUBSCRIPTION_KEY);
17 | }
18 |
19 | return {
20 | ...seed,
21 | ...recoveredState,
22 | };
23 | };
24 |
25 | const subscriptionSlice = createSlice({
26 | name: 'appsVulns',
27 | initialState: loadState(SUBSCRIPTION_STATE_SEED),
28 | reducers: {
29 | setSubscription(state, { payload }) {
30 | console.log('i am a subuscription');
31 | },
32 |
33 | setKey(state, { payload }) {
34 | state.key = payload.key;
35 | state.paid = true;
36 | },
37 |
38 | resetKey(state) {
39 | state.key = '';
40 | state.paid = false;
41 | },
42 | },
43 | });
44 |
45 | export const { setSubscription, setKey, resetKey } = subscriptionSlice.actions;
46 | export default subscriptionSlice.reducer;
47 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mana Security
6 |
17 |
18 |
19 |
20 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 |
5 | import App from './App';
6 | import osqueryRefreshThunk from './app/osqueryRefreshThunk';
7 | import syncRepoThunk from './app/repoThunk';
8 | import { store } from './app/store';
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/src/loading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Loading...
6 |
64 |
65 |
66 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/main.prod.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2010 LearnBoost
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /*!
18 | * Copyright (c) 2015, Salesforce.com, Inc.
19 | * All rights reserved.
20 | *
21 | * Redistribution and use in source and binary forms, with or without
22 | * modification, are permitted provided that the following conditions are met:
23 | *
24 | * 1. Redistributions of source code must retain the above copyright notice,
25 | * this list of conditions and the following disclaimer.
26 | *
27 | * 2. Redistributions in binary form must reproduce the above copyright notice,
28 | * this list of conditions and the following disclaimer in the documentation
29 | * and/or other materials provided with the distribution.
30 | *
31 | * 3. Neither the name of Salesforce.com nor the names of its contributors may
32 | * be used to endorse or promote products derived from this software without
33 | * specific prior written permission.
34 | *
35 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
37 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
38 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
39 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
40 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
41 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
42 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
43 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
44 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
45 | * POSSIBILITY OF SUCH DAMAGE.
46 | */
47 |
48 | /*!
49 | * Copyright (c) 2018, Salesforce.com, Inc.
50 | * All rights reserved.
51 | *
52 | * Redistribution and use in source and binary forms, with or without
53 | * modification, are permitted provided that the following conditions are met:
54 | *
55 | * 1. Redistributions of source code must retain the above copyright notice,
56 | * this list of conditions and the following disclaimer.
57 | *
58 | * 2. Redistributions in binary form must reproduce the above copyright notice,
59 | * this list of conditions and the following disclaimer in the documentation
60 | * and/or other materials provided with the distribution.
61 | *
62 | * 3. Neither the name of Salesforce.com nor the names of its contributors may
63 | * be used to endorse or promote products derived from this software without
64 | * specific prior written permission.
65 | *
66 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
67 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
68 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
69 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
70 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
71 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
72 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
73 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
74 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
75 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
76 | * POSSIBILITY OF SUCH DAMAGE.
77 | */
78 |
79 | /*!
80 | * mime-db
81 | * Copyright(c) 2014 Jonathan Ong
82 | * MIT Licensed
83 | */
84 |
85 | /*!
86 | * mime-types
87 | * Copyright(c) 2014 Jonathan Ong
88 | * Copyright(c) 2015 Douglas Christopher Wilson
89 | * MIT Licensed
90 | */
91 |
92 | /*! *****************************************************************************
93 | Copyright (c) Microsoft Corporation.
94 |
95 | Permission to use, copy, modify, and/or distribute this software for any
96 | purpose with or without fee is hereby granted.
97 |
98 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
99 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
100 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
101 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
102 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
103 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
104 | PERFORMANCE OF THIS SOFTWARE.
105 | ***************************************************************************** */
106 |
107 | /*! safe-buffer. MIT License. Feross Aboukhadijeh */
108 |
109 | /** @license URI.js v4.4.1 (c) 2011 Gary Court. License: http://github.com/garycourt/uri-js */
110 |
--------------------------------------------------------------------------------
/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mana-security",
3 | "productName": "Mana Security",
4 | "version": "2.4.2",
5 | "description": "Vulnerability management for macOS",
6 | "main": "./main.prod.js",
7 | "author": {
8 | "name": "Mana Security, Inc.",
9 | "email": "support@manasecurity.com",
10 | "url": "https://manasecurity.com/"
11 | },
12 | "scripts": {
13 | "electron-rebuild": "node -r ../.erb/scripts/BabelRegister.js ../.erb/scripts/ElectronRebuild.js",
14 | "postinstall": "yarn electron-rebuild"
15 | },
16 | "license": "MIT",
17 | "dependencies": {}
18 | }
19 |
--------------------------------------------------------------------------------
/src/styles/base.css:
--------------------------------------------------------------------------------
1 | @import './fonts.css';
2 |
3 | body {
4 | position: relative;
5 | color: black;
6 | height: 100vh;
7 | font-family: sans-serif;
8 | overflow-y: hidden;
9 | display: flex;
10 | justify-content: top;
11 | align-items: left;
12 | font-family: "SuisseIntl";
13 | font-style: normal;
14 | font-weight: normal;;
15 | }
16 |
17 | .bg-mana-gray {
18 | background-color: #ebebeb !important;
19 | }
20 |
21 | button {
22 | padding: 10px 20px;
23 | border-radius: 10px;
24 | border: none;
25 | appearance: none;
26 | font-size: 1.3rem;
27 | transition: transform ease-in 0.1s;
28 | cursor: pointer;
29 | }
30 |
31 | button:hover {
32 | /* transform: scale(1.05); */
33 | }
34 |
35 | li {
36 | list-style: none;
37 | }
38 |
39 | a {
40 | color: #1890ff !important;
41 | text-decoration: none;
42 | height: fit-content;
43 | width: fit-content;
44 | }
45 |
46 | a:hover {
47 | opacity: 1;
48 | text-decoration: none;
49 | }
50 |
51 | .blue {
52 | color: #0A40FF !important;
53 | }
54 |
55 | #root {
56 | width: 100%;
57 | }
58 |
59 | .ant-tabs-nav {
60 | margin-bottom: 0 !important;
61 | }
62 |
63 | .window-header {
64 | -webkit-app-region: drag;
65 | }
66 |
67 | .p-20px {
68 | padding: 20px;
69 | }
70 |
71 | .m-20px {
72 | margin: 20px;
73 | }
74 |
75 | .app-icon-shadow {
76 | filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.04))
77 | drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.08))
78 | drop-shadow(0px 12px 20px rgba(0, 0, 0, 0.08));
79 | }
80 |
81 | .text-black-important {
82 | color: #000 !important;
83 | }
84 |
85 | .text-red-important {
86 | color: #EE360E !important;
87 | }
88 |
89 | .text-gray-important {
90 | color: gray !important;
91 | }
92 |
93 | .capitalize-first-letter:first-letter {
94 | text-transform: uppercase;
95 | }
96 |
--------------------------------------------------------------------------------
/src/styles/dashboard.css:
--------------------------------------------------------------------------------
1 | .risk-status-low {
2 | color: #25C448;
3 | }
4 |
5 | .border-risk-status-low {
6 | border-color: #25C448;
7 | }
8 |
9 | .bg-risk-status-low {
10 | background-color: #25C448;
11 | }
12 |
13 | .risk-status-medium {
14 | color: #EE940E;
15 | }
16 |
17 | .border-risk-status-medium {
18 | border-color: #EE940E;
19 | }
20 |
21 | .bg-risk-status-medium {
22 | background-color: #EE940E;
23 | }
24 |
25 | .risk-status-high {
26 | color: #EE360E;
27 | }
28 |
29 | .border-risk-status-high {
30 | border-color: #EE360E;
31 | }
32 |
33 | .bg-risk-status-high {
34 | background-color: #EE360E;
35 | }
36 |
37 | .risk-status {
38 | color: #EE360E;
39 | font-size: 96px;
40 | line-height: 90%;
41 | letter-spacing: -0.08em;
42 | text-transform: uppercase;
43 | font-family: "SuisseIntl Mono";
44 | }
45 |
46 | .dashboard {
47 | overflow-y: auto;
48 | }
49 |
50 | .border-5px {
51 | border-radius: 5px;
52 | }
53 |
54 | .dashboard-item {
55 | background-color: white;
56 | border-radius: 5px;
57 | margin: 10px 0;
58 | padding: 20px;
59 | }
60 |
61 | .dashboard-chart {
62 | background-color: white;
63 | border-radius: 5px;
64 | }
65 |
66 | .dashboard-app-vuln {
67 | background-color: white;
68 | height: 220px;
69 | }
70 |
71 | .app-risk-high {
72 | height: 16px;
73 | width: 16px;
74 | background-color: #EE5F0E;
75 | }
76 |
77 | .app-risk-medium {
78 | height: 12px;
79 | width: 12px;
80 | background-color: #EE940E;
81 | }
82 |
83 | .app-risk-low {
84 | height: 8px;
85 | width: 8px;
86 | background-color: #25C448;
87 | }
88 |
89 | .min-w-chart {
90 | min-width: 2%;
91 | }
92 |
--------------------------------------------------------------------------------
/src/styles/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'SuisseIntl';
3 | font-style: normal;
4 | font-weight: normal;
5 | src: local('SuisseIntl'), url('../../assets/suisseintl-regular.woff') format('woff');
6 | }
7 |
8 | @font-face {
9 | font-family: 'SuisseIntl Mono';
10 | font-style: normal;
11 | font-weight: normal;
12 | src: local('SuisseIntl Mono'), url('../../assets/suisseintlmono-regular.woff') format('woff');
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/nvd/filterRelevantCVENames.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Finds all vulns' CVEs, that affect a given app.
3 | *
4 | * @param {str} appCPE app's name in CPE format.
5 | * @param {List} vulns repo of vulns.
6 | * @returns List of strings each containing a CVE id.
7 | */
8 | export default function filterRelevantCVENames(appCPE, vulns) {
9 | return vulns.reduce((buff, vuln) => {
10 | if (vuln.versions.filter(x => x.cpe === appCPE).length) {
11 | buff.push(vuln.cve);
12 | }
13 | return buff;
14 | }, []);
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/nvd/filterRelevantCVENames.spec.js:
--------------------------------------------------------------------------------
1 | import filterRelevantCVENames from './filterRelevantCVENames'
2 |
3 |
4 | test('single FF vuln\'s CVE appears in results', () => {
5 | const ffSingleVulnList = [{
6 | cve: "CVE-2042-0001",
7 | versions: [{
8 | cpe: "a:mozilla:firefox"
9 | }]
10 | }]
11 |
12 | expect(filterRelevantCVENames("a:mozilla:firefox", ffSingleVulnList)).toEqual(["CVE-2042-0001"])
13 | })
14 |
15 |
16 | test('two FF vulns\' CVE appears in results', () => {
17 | const ffSingleVulnList = [
18 | {
19 | cve: "CVE-2042-0001",
20 | versions: [{
21 | cpe: "a:mozilla:firefox"
22 | }]
23 | },
24 | {
25 | cve: "CVE-2042-0002",
26 | versions: [{
27 | cpe: "a:mozilla:firefox"
28 | }]
29 | },
30 | ]
31 |
32 | expect(filterRelevantCVENames("a:mozilla:firefox", ffSingleVulnList)).toEqual([
33 | "CVE-2042-0001",
34 | "CVE-2042-0002"
35 | ])
36 | })
37 |
38 |
39 | test('chrome\'s vuln CVE id does not appears in results for FF', () => {
40 | const ffSingleVulnList = [{
41 | cve: "CVE-2042-0001",
42 | versions: [{
43 | cpe: "a:google:chrome"
44 | }]
45 | }]
46 |
47 | expect(filterRelevantCVENames("a:mozilla:firefox", ffSingleVulnList)).toEqual([])
48 | })
49 |
--------------------------------------------------------------------------------
/src/utils/nvd/getCPEName.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets a CPE name for a given app object.
3 | *
4 | * @param {Dict} app an app object supported by Mana.
5 | * @returns string with a corresponding CPE-name.
6 | */
7 | export default function getCPEName(app) {
8 | return `${app.cpe_part}:${app.cpe_vendor}:${app.cpe_product}`;
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/nvd/getCPEName.spec.js:
--------------------------------------------------------------------------------
1 | import getCPEName from './getCPEName'
2 |
3 | test('firefox transforms into a:mozilla:firefox', () => {
4 | const app = {
5 | cpe_part: "a",
6 | cpe_vendor: "mozilla",
7 | cpe_product: "firefox"
8 | }
9 | expect(getCPEName(app)).toEqual('a:mozilla:firefox')
10 | })
11 |
12 |
13 | test('OS transforms into o:apple:mac_os_x', () => {
14 | const app = {
15 | cpe_part: "o",
16 | cpe_vendor: "apple",
17 | cpe_product: "mac_os_x"
18 | }
19 | expect(getCPEName(app)).toEqual('o:apple:mac_os_x')
20 | })
21 |
--------------------------------------------------------------------------------
/src/utils/osquery/filterByNotMatchingName.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Filters out osquery apps that Mana doesn't supports.
3 | *
4 | * @param {List} hostAppsList list of osquery apps
5 | * @param {Set} appRepoNamesSet set with names of all supported apps.
6 | * @returns List of osquery apps that are supported by Mana.
7 | */
8 | export default function filterByNotMatchingName(hostAppsList, appRepoNamesSet) {
9 | return hostAppsList.filter((x) =>
10 | x.aliases.some((y) => appRepoNamesSet.has(y))
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/osquery/osqueryi.js:
--------------------------------------------------------------------------------
1 | import { exec, execSync } from 'child_process';
2 | import { join as joinPath } from 'path';
3 | import rootPath from '../../../rootPath';
4 |
5 | // How much time osquery can run. After that the process will be terminated. Timeout is set in
6 | // milliseconds.
7 | const RUN_TIMEOUT = 3 * 1000; // 3 seconds
8 |
9 | /**
10 | * A black magic to build an absolute path to osqueryi binary.
11 | * Reference:
12 | * https://stackoverflow.com/questions/33152533/bundling-precompiled-binary-into-electron-app
13 | *
14 | * TODO Packaging doesn't work properly: seems it can't run the command. Probably due to macOS's
15 | * sandbox.
16 | */
17 | let binPath =
18 | process?.env?.NODE_ENV !== 'development'
19 | ? joinPath(rootPath(), 'bin')
20 | : joinPath(rootPath(), '..', 'resources');
21 | // osqueryi's path in test mode differs from others.
22 | if (process?.env?.NODE_ENV === 'test') {
23 | binPath = joinPath(rootPath(), 'resources');
24 | }
25 | const osqueryiCmd = `'${joinPath(binPath, 'osqueryi')}'`;
26 |
27 | /**
28 | * Executes osqueryi with a given query.
29 | *
30 | * @param {string} query - what to query from osqueryi
31 | * @returns output JSON-string
32 | */
33 | const runQuery = (query) => {
34 | const osqueryCommand = [osqueryiCmd, '--json', `'${query}'`].join(' ');
35 | try {
36 | const result = execSync(osqueryCommand, {
37 | timeout: RUN_TIMEOUT,
38 | });
39 | return [result, false];
40 | } catch (error) {
41 | console.error(`osquery launch failed with error: %O`, error);
42 | return [[], true];
43 | }
44 | };
45 |
46 | /**
47 | * Loads a list of host's apps.
48 | *
49 | * TODO Validate JSON with jsonschema:
50 | * https://www.npmjs.com/package/jsonschema
51 | *
52 | * @returns JSON list with apps.
53 | */
54 | const loadApps = () => {
55 | const [appsString, error] = runQuery(`
56 | select bundle_executable,
57 | bundle_name,
58 | bundle_short_version,
59 | bundle_version,
60 | display_name,
61 | last_opened_time,
62 | name,
63 | path
64 | from apps
65 | where path LIKE "/Applications/%" OR path LIKE "/Users/%/Applications/%"`);
66 | if (error) return [[], error];
67 |
68 | const apps = JSON.parse(appsString);
69 | return [apps, error];
70 | };
71 |
72 | const loadOS = () => {
73 | const [queryForOS, error] = runQuery(`
74 | select major,
75 | minor,
76 | patch,
77 | build
78 | from os_version
79 | limit 1`);
80 | if (error) return [{}, error];
81 | const osInfo = JSON.parse(queryForOS)[0];
82 |
83 | return [osInfo, error];
84 | };
85 |
86 | export { loadApps, loadOS };
87 |
--------------------------------------------------------------------------------
/src/utils/osquery/tests/filterByNotMatchingName.spec.js:
--------------------------------------------------------------------------------
1 | import filterByNotMatchingName from '../filterByNotMatchingName';
2 | import { OsqueryApp } from '../../../features/appsVulnerabilities/types/osqueryApp';
3 |
4 | test('unsupported chrome should be dropped', () => {
5 | const ff = new OsqueryApp({
6 | name: 'Firefox.app',
7 | bundle_name: 'Firefox',
8 | });
9 | const chrome = new OsqueryApp({
10 | name: 'Chrome.app',
11 | bundle_name: 'Chrome',
12 | });
13 |
14 | const result = filterByNotMatchingName([ff, chrome], new Set(['firefox']));
15 | expect(result.length).toBe(1);
16 | expect(result[0].aliases.includes('firefox')).toBeTruthy();
17 | expect(result[0].aliases.length === 1).toBeTruthy();
18 | });
19 |
20 | test('should return an empty list if no apps are supported', () => {
21 | const ff = new OsqueryApp({
22 | name: 'Firefox.app',
23 | bundle_name: 'Firefox',
24 | });
25 | const result = filterByNotMatchingName([ff], new Set(['chrome']));
26 | expect(result.length).toBe(0);
27 | });
28 |
--------------------------------------------------------------------------------
/src/utils/persist/analyticsAsync.ts:
--------------------------------------------------------------------------------
1 | import { STATE_STORAGE_ANALYTICS_KEY } from '../../configs';
2 | import { savePersistentState } from './persistHelpers';
3 |
4 | export default async function analyticsWriteAsync(analytics) {
5 | console.log('persisting analytics...');
6 | savePersistentState(STATE_STORAGE_ANALYTICS_KEY, analytics);
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/persist/appsVulnsAsync.ts:
--------------------------------------------------------------------------------
1 | import { STATE_STORAGE_APPSVULNS_KEY } from '../../configs';
2 | import { savePersistentState } from './persistHelpers';
3 |
4 | export default async function appsVulnsWriteAsync(appsVulns) {
5 | console.log('persisting apps vulns...');
6 | savePersistentState(STATE_STORAGE_APPSVULNS_KEY, appsVulns);
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/persist/persistHelpers.ts:
--------------------------------------------------------------------------------
1 | import Store from 'electron-store';
2 | import {
3 | STATE_STORAGE_ANALYTICS_KEY,
4 | STATE_STORAGE_APPSVULNS_KEY,
5 | STATE_STORAGE_STATUS_KEY,
6 | } from '../../configs';
7 |
8 | const persistenStore = new Store({
9 | name: 'manaconfig',
10 | fileExtension: 'json',
11 | });
12 |
13 | /**
14 | * Saves a state of apps' vulns on disk.
15 | *
16 | * @param {Dict} value apps' state part.
17 | */
18 | export const savePersistentState = (key, value) => {
19 | persistenStore.set(key, value);
20 | };
21 |
22 | /**
23 | * Checks if the state of apps' vulns is on the disk.
24 | *
25 | * @returns 'true' if state of apps' vulns is on the disk. 'false' otherwise.
26 | */
27 | export const hasPersistentState = (key) => {
28 | return persistenStore.has(key);
29 | };
30 |
31 | /**
32 | * Loads a state of apps' vulns from the disk.
33 | *
34 | * @returns a state of apps' vulns.
35 | */
36 | export const loadPersistentState = (key) => {
37 | const appState = persistenStore.get(key, false);
38 | return appState;
39 | };
40 |
--------------------------------------------------------------------------------
/src/utils/persist/statusAsync.ts:
--------------------------------------------------------------------------------
1 | import { STATE_STORAGE_STATUS_KEY } from '../../configs';
2 | import { savePersistentState } from './persistHelpers';
3 |
4 | export default async function statusWriteAsync(status) {
5 | savePersistentState(STATE_STORAGE_STATUS_KEY, status);
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/persist/subscriptionAsync.ts:
--------------------------------------------------------------------------------
1 | import { STATE_STORAGE_SUBSCRIPTION_KEY } from '../../configs';
2 | import { savePersistentState } from './persistHelpers';
3 |
4 | export default async function subscriptionWriteAsync(subscription) {
5 | savePersistentState(STATE_STORAGE_SUBSCRIPTION_KEY, subscription);
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/set/difference.js:
--------------------------------------------------------------------------------
1 | export default function difference(setA, setB) {
2 | // const setDifference = new Set(setA);
3 | // setB.keys().reduce((set, x) => set.delete(x), setDifference);
4 | // return setDifference;
5 | return new Set([...setA].filter((x) => !setB.has(x)));
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/set/difference.spec.js:
--------------------------------------------------------------------------------
1 | import difference from './difference';
2 |
3 | test('common item is not present in difference', () => {
4 | const aMinusB = difference(new Set(['a', 'b']), new Set(['b', 'c']));
5 | expect(aMinusB).toEqual(new Set(['a']));
6 | });
7 |
8 | test('a set with no common items should remain the same', () => {
9 | const aMinusB = difference(new Set(['a', 'b']), new Set(['c']));
10 | expect(aMinusB).toEqual(new Set(['a', 'b']));
11 | });
12 |
--------------------------------------------------------------------------------
/src/utils/set/intersection.js:
--------------------------------------------------------------------------------
1 | export default function intersection(setA, setB) {
2 | return new Set([...setA].filter((x) => setB.has(x)));
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/set/intersection.spec.js:
--------------------------------------------------------------------------------
1 | import intersection from './intersection';
2 |
3 | test('given sets [a, b] and [b, c] fn results are [b]', () => {
4 | const anIntersect = intersection(new Set(['a', 'b']), new Set(['b', 'c']));
5 | expect(anIntersect).toEqual(new Set(['b']));
6 | });
7 |
8 | test('given sets [a, b] and [c, d] fn results are []', () => {
9 | const anIntersect = intersection(new Set(['a', 'b']), new Set(['c', 'd']));
10 | expect(anIntersect).toEqual(new Set([]));
11 | });
12 |
--------------------------------------------------------------------------------
/src/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "CommonJS",
5 | "lib": ["dom", "esnext"],
6 | "declaration": true,
7 | "declarationMap": true,
8 | "noEmit": true,
9 | "jsx": "react",
10 | "strict": true,
11 | "pretty": true,
12 | "sourceMap": true,
13 | /* Additional Checks */
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "noImplicitReturns": true,
17 | "noFallthroughCasesInSwitch": true,
18 | /* Module Resolution Options */
19 | "moduleResolution": "node",
20 | "esModuleInterop": true,
21 | "allowSyntheticDefaultImports": true,
22 | "resolveJsonModule": true,
23 | "allowJs": true
24 | },
25 | "exclude": [
26 | "test",
27 | "release",
28 | "src/main.prod.js",
29 | "src/renderer.prod.js",
30 | "src/dist",
31 | ".erb/dll"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------