├── .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 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/house.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/pro-plan-promo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/upgrade-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/vulns.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 |
66 | 67 |
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 |
10 |

 

11 |

 

12 |
13 | 14 | 15 | 16 |
17 |

 

18 |
19 |

 

20 |

 

21 |
22 |
23 | 24 | 25 |
26 |

 

27 |
28 |

 

29 |

 

30 |

 

31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 |
 
40 |
41 |
42 |
43 |
44 |
45 | ); 46 | }; 47 | 48 | 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 |
34 |
35 |

Activation key:

36 | 43 |

44 | You should receive an activation key after a successful payment or 45 | trial request. If you did not find the code, check the spam folder 46 | or contact us via{' '} 47 | help@manasecurity.com. 48 |

49 | 56 | 64 |
65 |
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 |
22 |
23 |

Email:

24 | 32 |

33 | We will only use this address to contact you about security or 34 | billing changes on your account. No marketing emails, no sales 35 | emails, nothing like that. 36 |

37 | 44 | 52 |
53 |
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 |
67 |
68 | Starting 69 |
70 |
71 |
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 | --------------------------------------------------------------------------------