Skip to content

Commit

Permalink
Merge pull request #78 from SocketDev/12-factor-fix
Browse files Browse the repository at this point in the history
Fixes for offline / npm
  • Loading branch information
bmeck authored Aug 22, 2023
2 parents 03ff3cc + c704f15 commit 2e3c389
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 63 deletions.
36 changes: 35 additions & 1 deletion lib/commands/login/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import prompts from 'prompts'
import terminalLink from 'terminal-link'

import { AuthError, InputError } from '../../utils/errors.js'
import { prepareFlags } from '../../utils/flags.js'
import { printFlagList } from '../../utils/formatting.js'
import { FREE_API_KEY, setupSdk } from '../../utils/sdk.js'
import { getSetting, updateSetting } from '../../utils/settings.js'

Expand All @@ -14,19 +16,36 @@ const description = 'Socket API login'
export const login = {
description,
run: async (argv, importMeta, { parentName }) => {
const flags = prepareFlags({
apiBaseUrl: {
type: 'string',
description: 'API server to connect to for login',
},
apiProxy: {
type: 'string',
description: 'Proxy to use when making connection to API server'
}
})
const name = parentName + ' login'
const cli = meow(`
Usage
$ ${name}
Logs into the Socket API by prompting for an API key
Options
${printFlagList({
'api-base-url': flags.apiBaseUrl.description,
'api-proxy': flags.apiProxy.description
}, 8)}
Examples
$ ${name}
`, {
argv,
description,
importMeta,
flags
})

/**
Expand Down Expand Up @@ -58,13 +77,27 @@ export const login = {

const apiKey = result.apiKey || FREE_API_KEY

/**
* @type {string | null | undefined}
*/
let apiBaseUrl = cli.flags.apiBaseUrl
apiBaseUrl ??= getSetting('apiBaseUrl') ??
undefined

/**
* @type {string | null | undefined}
*/
let apiProxy = cli.flags.apiProxy
apiProxy ??= getSetting('apiProxy') ??
undefined

const spinner = ora('Verifying API key...').start()

/** @type {import('@socketsecurity/sdk').SocketSdkReturnType<'getOrganizations'>['data']} */
let orgs

try {
const sdk = await setupSdk(apiKey)
const sdk = await setupSdk(apiKey, apiBaseUrl, apiProxy)
const result = await sdk.getOrganizations()
if (!result.success) throw new AuthError()
orgs = result.data
Expand Down Expand Up @@ -131,6 +164,7 @@ export const login = {
updateSetting('enforcedOrgs', enforcedOrgs)
const oldKey = getSetting('apiKey')
updateSetting('apiKey', apiKey)
updateSetting('apiBaseUrl', apiBaseUrl)
spinner.succeed(`API credentials ${oldKey ? 'updated' : 'set'}`)
}
}
2 changes: 2 additions & 0 deletions lib/commands/logout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const logout = {
if (cli.input.length) cli.showHelp()

updateSetting('apiKey', null)
updateSetting('apiBaseUrl', null)
updateSetting('apiProxy', null)
updateSetting('enforcedOrgs', null)
ora('Successfully logged out').succeed()
}
Expand Down
1 change: 0 additions & 1 deletion lib/commands/report/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ async function setupCommand (name, description, argv, importMeta) {
}
})

// TODO: setupSdk(getDefaultKey() || FREE_API_KEY)
const socketSdk = await setupSdk()
const supportedFiles = await socketSdk.getReportSupportedFiles()
.then(res => {
Expand Down
120 changes: 70 additions & 50 deletions lib/shadow/npm-injection.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,50 @@ try {
*/

const pubTokenPromise = sdkPromise.then(({ getDefaultKey, FREE_API_KEY }) => getDefaultKey() || FREE_API_KEY)
const apiKeySettingsPromise = sdkPromise.then(async ({ setupSdk }) => {
const sdk = await setupSdk(await pubTokenPromise)
const orgResult = await sdk.getOrganizations()
if (!orgResult.success) {
throw new Error('Failed to fetch Socket organization info: ' + orgResult.error.message)
}
/**
* @type {(Exclude<typeof orgResult.data.organizations[string], undefined>)[]}
*/
const orgs = []
for (const org of Object.values(orgResult.data.organizations)) {
if (org) {
orgs.push(org)
const apiKeySettingsInit = sdkPromise.then(async ({ setupSdk }) => {
try {
const sdk = await setupSdk(await pubTokenPromise)
const orgResult = await sdk.getOrganizations()
if (!orgResult.success) {
throw new Error('Failed to fetch Socket organization info: ' + orgResult.error.message)
}
/**
* @type {(Exclude<typeof orgResult.data.organizations[string], undefined>)[]}
*/
const orgs = []
for (const org of Object.values(orgResult.data.organizations)) {
if (org) {
orgs.push(org)
}
}
const result = await sdk.postSettings(orgs.map(org => {
return {
organization: org.id
}
}))
if (!result.success) {
throw new Error('Failed to fetch API key settings: ' + result.error.message)
}
}
const result = await sdk.postSettings(orgs.map(org => {
return {
organization: org.id
orgs,
settings: result.data
}
}))
if (!result.success) {
throw new Error('Failed to fetch API key settings: ' + result.error.message)
}
return {
orgs,
settings: result.data
} catch (e) {
if (e && typeof e === 'object' && 'cause' in e) {
const cause = e.cause
if (isErrnoException(cause)) {
if (cause.code === 'ENOTFOUND' || cause.code === 'ECONNREFUSED') {
throw new Error('Unable to connect to socket.dev, ensure internet connectivity before retrying', {
cause: e
})
}
}
}
throw e
}
})
// mark apiKeySettingsInit as handled
apiKeySettingsInit.catch(() => {})

/**
*
Expand All @@ -78,42 +94,43 @@ async function findSocketYML () {
const fs = require('fs/promises')
while (dir !== prevDir) {
const ymlPath = path.join(dir, 'socket.yml')
const yml = fs.readFile(ymlPath, 'utf-8')
// mark as handled
const yml = fs.readFile(ymlPath, 'utf-8').catch(() => {})
yml.catch(() => {})
const yamlPath = path.join(dir, 'socket.yaml')
const yaml = fs.readFile(yamlPath, 'utf-8')
// mark as handled
const yaml = fs.readFile(yamlPath, 'utf-8').catch(() => {})
try {
const txt = await yml
if (txt != null) {
return {
path: ymlPath,
parsed: config.parseSocketConfig(txt)
}
}
} catch (e) {
yaml.catch(() => {})
/**
* @param {unknown} e
* @returns {boolean}
*/
function checkFileFoundError (e) {
if (isErrnoException(e)) {
if (e.code !== 'ENOENT' && e.code !== 'EISDIR') {
throw e
}
} else {
return false
}
return true
}
try {
return {
path: ymlPath,
parsed: config.parseSocketConfig(await yml)
}
} catch (e) {
if (checkFileFoundError(e)) {
throw new Error('Found file but was unable to parse ' + ymlPath)
}
}
try {
const txt = await yaml
if (txt != null) {
return {
path: yamlPath,
parsed: config.parseSocketConfig(txt)
}
return {
path: ymlPath,
parsed: config.parseSocketConfig(await yaml)
}
} catch (e) {
if (isErrnoException(e)) {
if (e.code !== 'ENOENT' && e.code !== 'EISDIR') {
throw e
}
} else {
if (checkFileFoundError(e)) {
throw new Error('Found file but was unable to parse ' + yamlPath)
}
}
Expand All @@ -124,11 +141,12 @@ async function findSocketYML () {
}

/**
* @type {Promise<ReturnType<import('../utils/issue-rules.cjs')['createIssueUXLookup']>>}
* @type {Promise<ReturnType<import('../utils/issue-rules.cjs')['createIssueUXLookup']> | undefined>}
*/
const uxLookupPromise = settingsPromise.then(async ({ getSetting }) => {
const uxLookupInit = settingsPromise.then(async ({ getSetting }) => {
const enforcedOrgs = getSetting('enforcedOrgs') ?? []
const { orgs, settings } = await apiKeySettingsPromise
const remoteSettings = await apiKeySettingsInit
const { orgs, settings } = remoteSettings

// remove any organizations not being enforced
for (const [i, org] of orgs.entries()) {
Expand All @@ -152,6 +170,8 @@ const uxLookupPromise = settingsPromise.then(async ({ getSetting }) => {
}
return createIssueUXLookup(settings)
})
// mark uxLookupInit as handled
uxLookupInit.catch(() => {})

// shadow `npm` and `npx` to mitigate subshells
require('./link.cjs')(fs.realpathSync(path.join(__dirname, 'bin')), 'npm')
Expand Down Expand Up @@ -506,7 +526,7 @@ async function packagesHaveRiskyIssues (safeArb, _registry, pkgs, ora = null, _i
const pkgDatas = []
try {
// TODO: determine org based on cwd, pass in
const uxLookup = await uxLookupPromise
const uxLookup = await uxLookupInit

for await (const pkgData of batchScan(pkgs.map(pkg => pkg.pkgid))) {
/**
Expand Down
5 changes: 3 additions & 2 deletions lib/shadow/tty-server.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const path = require('path')
const { PassThrough } = require('stream')
const { isErrnoException } = require('../utils/type-helpers.cjs')

const ipc_version = require('../../package.json').version
const { isErrnoException } = require('../utils/type-helpers.cjs')

/**
* @typedef {import('stream').Readable} Readable
Expand All @@ -22,7 +23,7 @@ module.exports = async function createTTYServer (colorLevel, isInteractive, npml
* @type {import('readline')}
*/
let readline
const isSTDINInteractive = true || isInteractive
const isSTDINInteractive = isInteractive
if (!isSTDINInteractive && TTY_IPC) {
return {
async captureTTY (mutexFn) {
Expand Down
44 changes: 38 additions & 6 deletions lib/utils/sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,47 @@ let defaultKey

/** @returns {string | undefined} */
export function getDefaultKey () {
defaultKey = getSetting('apiKey') || process.env['SOCKET_SECURITY_API_KEY'] || defaultKey
defaultKey = process.env['SOCKET_SECURITY_API_KEY'] || getSetting('apiKey') || defaultKey
return defaultKey
}

/**
* The API server that should be used for operations
*
* @type {string | undefined}
*/
let defaultAPIBaseUrl

/**
* @returns {string | undefined}
*/
export function getDefaultAPIBaseUrl () {
defaultAPIBaseUrl = process.env['SOCKET_SECURITY_API_BASE_URL'] || getSetting('apiBaseUrl') || undefined
return defaultAPIBaseUrl
}

/**
* The API server that should be used for operations
*
* @type {string | undefined}
*/
let defaultApiProxy

/**
* @returns {string | undefined}
*/
export function getDefaultHTTPProxy () {
defaultApiProxy = process.env['SOCKET_SECURITY_API_PROXY'] || getSetting('apiProxy') || undefined
return defaultApiProxy
}

/**
* @param {string} [apiKey]
* @param {string} [apiBaseUrl]
* @param {string} [proxy]
* @returns {Promise<import('@socketsecurity/sdk').SocketSdk>}
*/
export async function setupSdk (apiKey = getDefaultKey()) {
export async function setupSdk (apiKey = getDefaultKey(), apiBaseUrl = getDefaultAPIBaseUrl(), proxy = getDefaultHTTPProxy()) {
if (apiKey == null && isInteractive()) {
/**
* @type {{ apiKey: string }}
Expand All @@ -49,11 +81,11 @@ export async function setupSdk (apiKey = getDefaultKey()) {
/** @type {import('@socketsecurity/sdk').SocketSdkOptions["agent"]} */
let agent

if (process.env['SOCKET_SECURITY_API_PROXY']) {
if (proxy) {
const { HttpProxyAgent, HttpsProxyAgent } = await import('hpagent')
agent = {
http: new HttpProxyAgent({ proxy: process.env['SOCKET_SECURITY_API_PROXY'] }),
https: new HttpsProxyAgent({ proxy: process.env['SOCKET_SECURITY_API_PROXY'] }),
http: new HttpProxyAgent({ proxy }),
https: new HttpsProxyAgent({ proxy }),
}
}
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json')
Expand All @@ -62,7 +94,7 @@ export async function setupSdk (apiKey = getDefaultKey()) {
/** @type {import('@socketsecurity/sdk').SocketSdkOptions} */
const sdkOptions = {
agent,
baseUrl: process.env['SOCKET_SECURITY_API_BASE_URL'],
baseUrl: apiBaseUrl,
userAgent: createUserAgentFromPkgJson(JSON.parse(packageJson))
}

Expand Down
2 changes: 1 addition & 1 deletion lib/utils/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const settingsPath = path.join(dataHome, 'socket', 'settings')
* @typedef {Record<string, boolean | {action: 'error' | 'warn' | 'ignore' | 'defer'}>} IssueRules
*/

/** @type {{apiKey?: string | null, enforcedOrgs?: string[] | null}} */
/** @type {{apiKey?: string | null, enforcedOrgs?: string[] | null, apiBaseUrl?: string | null, apiProxy?: string | null}} */
let settings = {}

if (fs.existsSync(settingsPath)) {
Expand Down
4 changes: 4 additions & 0 deletions test/socket-npm-fixtures/lacking-typosquat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"dependencies": {
}
}
Loading

0 comments on commit 2e3c389

Please sign in to comment.