Socket
Socket
Sign inDemoInstall

ssh2-sftp-client

Package Overview
Dependencies
30
Maintainers
1
Versions
73
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 9.1.0 to 10.0.0

20

package.json
{
"name": "ssh2-sftp-client",
"version": "9.1.0",
"version": "10.0.0",
"description": "ssh2 sftp client for node",

@@ -21,3 +21,3 @@ "main": "src/index.js",

"engines": {
"node": ">=10.24.1"
"node": ">=16.20.2"
},

@@ -34,3 +34,3 @@ "author": "Tim Cross",

"devDependencies": {
"chai": "^4.3.6",
"chai": "^4.3.10",
"chai-as-promised": "^7.1.1",

@@ -40,14 +40,14 @@ "chai-subset": "^1.6.0",

"dotenv": "^16.0.0",
"eslint": "^8.17.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-mocha": "^10.0.3",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-mocha": "^10.2.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-unicorn": "^46.0.0",
"eslint-plugin-unicorn": "^50.0.1",
"mocha": "^10.0.0",
"moment": "^2.29.1",
"nyc": "^15.1.0",
"prettier": "^2.6.1",
"prettier": "^3.0.3",
"through2": "^4.0.2",
"winston": "^3.6.0"
"winston": "^3.11.0"
},

@@ -57,4 +57,4 @@ "dependencies": {

"promise-retry": "^2.0.1",
"ssh2": "^1.12.0"
"ssh2": "^1.15.0"
}
}
'use strict';
const { Client } = require('ssh2');
const fs = require('fs');
const fs = require('node:fs');
const concat = require('concat-stream');
const promiseRetry = require('promise-retry');
const { join, parse } = require('path');
const { join, parse } = require('node:path');
const {

@@ -17,2 +17,3 @@ globalListener,

haveLocalCreate,
partition,
} = require('./utils');

@@ -23,6 +24,6 @@ const { errorCode } = require('./constants');

constructor(clientName) {
this.version = '9.0.4';
this.version = '10.0.0';
this.client = new Client();
this.sftp = undefined;
this.clientName = clientName ? clientName : 'sftp';
this.clientName = clientName || 'sftp';
this.endCalled = false;

@@ -34,3 +35,3 @@ this.errorHandled = false;

this.debug = undefined;
this.promiseLimit = 10;
this.client.on('close', globalListener(this, 'close'));

@@ -45,3 +46,3 @@ this.client.on('end', globalListener(this, 'end'));

this.debug(
`CLIENT[${this.clientName}]: ${msg} ${JSON.stringify(obj, null, ' ')}`
`CLIENT[${this.clientName}]: ${msg} ${JSON.stringify(obj, null, ' ')}`,
);

@@ -66,3 +67,3 @@ } else {

msg = `${name}: ${err}${retry}`;
code = eCode ? eCode : errorCode.generic;
code = eCode || errorCode.generic;
} else if (err.custom) {

@@ -73,15 +74,19 @@ msg = `${name}->${err.message}${retry}`;

switch (err.code) {
case 'ENOTFOUND':
case 'ENOTFOUND': {
msg = `${name}: Address lookup failed for host${retry}`;
break;
case 'ECONNREFUSED':
}
case 'ECONNREFUSED': {
msg = `${name}: Remote host refused connection${retry}`;
break;
case 'ECONNRESET':
}
case 'ECONNRESET': {
msg = `${name}: Remote host has reset the connection: ${err.message}${retry}`;
break;
default:
}
default: {
msg = `${name}: ${err.message}${retry}`;
}
}
code = err.code ? err.code : errorCode.generic;
code = err.code || errorCode.generic;
}

@@ -126,3 +131,2 @@ const newError = new Error(msg);

* @return {Promise<Object>} which will resolve to an sftp client object
*
*/

@@ -173,3 +177,2 @@ getConnection(config) {

* @return {Promise<Object>} which will resolve to an sftp client object
*
*/

@@ -186,2 +189,3 @@ async connect(config) {

}
this.promiseLimit = config.promiseLimit ?? 10;
if (this.sftp) {

@@ -191,3 +195,3 @@ throw this.fmtError(

'connect',
errorCode.connect
errorCode.connect,
);

@@ -197,3 +201,3 @@ }

retries: config.retries ?? 1,
factor: config.factor ?? 2,
factor: config.retry_factor ?? 2,
minTimeout: config.retry_minTimeout ?? 25000,

@@ -209,4 +213,5 @@ };

case 'ECONNREFUSED':
case 'ERR_SOCKET_BAD_PORT':
case 'ERR_SOCKET_BAD_PORT': {
throw err;
}
case undefined: {

@@ -222,4 +227,5 @@ if (

}
default:
default: {
retry(err);
}
}

@@ -300,3 +306,3 @@ }

return new Promise((resolve, reject) => {
let cb = (err, stats) => {
const cb = (err, stats) => {
if (err) {

@@ -352,3 +358,2 @@ if (err.code === 2 || err.code === 4) {

* @return {Promise<Object>} stats - attributes info
*
*/

@@ -373,3 +378,2 @@ async stat(remotePath) {

* @return {Promise<Object>} stats - attributes info
*
*/

@@ -455,5 +459,5 @@ async lstat(remotePath) {

rights: {
user: item.longname.slice(1, 4).replace(reg, ''),
group: item.longname.slice(4, 7).replace(reg, ''),
other: item.longname.slice(7, 10).replace(reg, ''),
user: item.longname.slice(1, 4).replaceAll(reg, ''),
group: item.longname.slice(4, 7).replaceAll(reg, ''),
other: item.longname.slice(7, 10).replaceAll(reg, ''),
},

@@ -509,3 +513,6 @@ owner: item.attrs.uid,

readStreamOptions: { ...options?.readStreamOptions, autoClose: true },
writeStreamOptions: { ...options?.writeStreamOptions, autoClose: true },
writeStreamOptions: {
...options?.writeStreamOptions,
autoClose: true,
},
pipeOptions: { ...options?.pipeOptions, end: true },

@@ -530,3 +537,5 @@ };

const localCheck = haveLocalCreate(dst);
if (!localCheck.status) {
if (localCheck.status) {
wtr = fs.createWriteStream(dst, options.writeStreamOptions);
} else {
reject(

@@ -536,8 +545,5 @@ this.fmtError(

'get',
localCheck.code
)
localCheck.code,
),
);
return;
} else {
wtr = fs.createWriteStream(dst, options.writeStreamOptions);
}

@@ -553,4 +559,4 @@ } else {

'get',
err.code
)
err.code,
),
);

@@ -581,2 +587,6 @@ });

*
* WARNING: The functionality of fastGet is heavily dependent on the capabilities
* of the remote SFTP server. Not all sftp server support or fully support this
* functionality. See the Platform Quirks & Warnings section of the README.
*
* @param {String} remotePath

@@ -612,3 +622,3 @@ * @param {String} localPath

if (ftype !== '-') {
const msg = `${!ftype ? 'No such file ' : 'Not a regular file'} ${remotePath}`;
const msg = `${ftype ? 'Not a regular file' : 'No such file '} ${remotePath}`;
throw this.fmtError(msg, 'fastGet', errorCode.badPath);

@@ -621,3 +631,3 @@ }

'fastGet',
errorCode.badPath
errorCode.badPath,
);

@@ -639,2 +649,6 @@ }

*
* WARNING: The fastPut functionality is heavily dependent on the capabilities of
* the remote sftp server. Many sftp servers do not support or do not fully support this
* functionality. See the Platform Quirks & Warnings section of the README for more details.
*
* @param {String} localPath - path to local file to put

@@ -659,4 +673,4 @@ * @param {String} remotePath - destination path for put file

'fastPut',
err.code
)
err.code,
),
);

@@ -682,3 +696,3 @@ }

'fastPut',
localCheck.code
localCheck.code,
);

@@ -689,3 +703,3 @@ } else if (localCheck.status && localExists(localPath) === 'd') {

'fastgPut',
errorCode.badPath
errorCode.badPath,
);

@@ -731,3 +745,7 @@ }

reject(
this.fmtError(`Write stream error: ${err.message} ${rPath}`, '_put', err.code)
this.fmtError(
`Write stream error: ${err.message} ${rPath}`,
'_put',
err.code,
),
);

@@ -753,4 +771,4 @@ });

'_put',
err.code
)
err.code,
),
);

@@ -776,3 +794,3 @@ });

'put',
localCheck.code
localCheck.code,
);

@@ -831,3 +849,3 @@ }

'append',
errorCode.badPath
errorCode.badPath,
);

@@ -840,3 +858,3 @@ }

'append',
errorCode.badPath
errorCode.badPath,
);

@@ -873,4 +891,4 @@ }

'_doMkdir',
errorCode.badPath
)
errorCode.badPath,
),
);

@@ -882,4 +900,4 @@ } else if (err.code === 2) {

'_doMkdir',
errorCode.badPath
)
errorCode.badPath,
),
);

@@ -908,3 +926,3 @@ } else {

'_mkdir',
errorCode.badPath
errorCode.badPath,
);

@@ -926,3 +944,3 @@ } else if (targetExists) {

'_mkdir',
errorCode.badPath
errorCode.badPath,
);

@@ -980,3 +998,3 @@ }

this.debugMsg(`_delFiles: path = ${path} fileList = ${fileList}`);
let pList = [];
const pList = [];
for (const f of fileList) {

@@ -997,4 +1015,4 @@ pList.push(this.delete(`${path}/${f.name}`, true, false));

this.debugMsg(`rmdir: dir = ${remoteDir} recursive = ${recursive}`);
let absPath = await normalizeRemotePath(this, remoteDir);
let existStatus = await this.exists(absPath);
const absPath = await normalizeRemotePath(this, remoteDir);
const existStatus = await this.exists(absPath);
this.debugMsg(`rmdir: ${absPath} existStatus = ${existStatus}`);

@@ -1005,3 +1023,3 @@ if (!existStatus) {

'rmdir',
errorCode.badPath
errorCode.badPath,
);

@@ -1013,3 +1031,3 @@ }

'rmdir',
errorCode.badPath
errorCode.badPath,
);

@@ -1021,3 +1039,3 @@ }

}
let listing = await this.list(absPath);
const listing = await this.list(absPath);
this.debugMsg(`rmdir: listing count = ${listing.length}`);

@@ -1028,5 +1046,5 @@ if (!listing.length) {

}
let fileList = listing.filter((i) => i.type !== 'd');
const fileList = listing.filter((i) => i.type !== 'd');
this.debugMsg(`rmdir: dir content files to remove = ${fileList.length}`);
let dirList = listing.filter((i) => i.type === 'd');
const dirList = listing.filter((i) => i.type === 'd');
this.debugMsg(`rmdir: sub-directories to remove = ${dirList.length}`);

@@ -1055,3 +1073,2 @@ await _delFiles(absPath, fileList);

* @return {Promise<String>} with string 'Successfully deleted file' once resolved
*
*/

@@ -1091,3 +1108,2 @@ delete(remotePath, notFoundOK = false, addListeners = true) {

* @return {Promise<String>}
*
*/

@@ -1107,4 +1123,4 @@ rename(fPath, tPath, addListeners = true) {

'_rename',
err.code
)
err.code,
),
);

@@ -1133,3 +1149,2 @@ }

* @return {Promise<String>}
*
*/

@@ -1149,4 +1164,4 @@ posixRename(fPath, tPath, addListeners = true) {

'_posixRename',
err.code
)
err.code,
),
);

@@ -1215,4 +1230,4 @@ }

const getRemoteStatus = async (dstDir) => {
let absDstDir = await normalizeRemotePath(this, dstDir);
let status = await this.exists(absDstDir);
const absDstDir = await normalizeRemotePath(this, dstDir);
const status = await this.exists(absDstDir);
if (status && status !== 'd') {

@@ -1222,3 +1237,3 @@ throw this.fmtError(

'getRemoteStatus',
errorCode.badPath
errorCode.badPath,
);

@@ -1235,3 +1250,3 @@ }

'getLocalStatus',
errorCode.badPath
errorCode.badPath,
);

@@ -1243,3 +1258,3 @@ }

'getLocalStatus',
errorCode.badPath
errorCode.badPath,
);

@@ -1250,29 +1265,32 @@ }

const uploadFiles = (srcDir, dstDir, fileList, useFastput) => {
let listeners;
return new Promise((resolve, reject) => {
listeners = addTempListeners(this, 'uploadFiles', reject);
let uploads = [];
const uploadFiles = async (srcDir, dstDir, fileList, useFastput) => {
let listeners = addTempListeners(this, 'uploadFiles');
try {
const uploadList = [];
for (const f of fileList) {
const newSrc = join(srcDir, f.name);
const newDst = `${dstDir}/${f.name}`;
if (f.isFile()) {
if (useFastput) {
uploads.push(this._fastPut(newSrc, newDst, null, false));
} else {
uploads.push(this._put(newSrc, newDst, null, false));
}
this.client.emit('upload', { source: newSrc, destination: newDst });
} else {
this.debugMsg(`uploadFiles: File ignored: ${f.name} not a regular file`);
const src = join(srcDir, f.name);
const dst = `${dstDir}/${f.name}`;
uploadList.push([src, dst]);
}
const uploadGroups = partition(uploadList, this.promiseLimit);
const func = useFastput ? this._fastPut.bind(this) : this._put.bind(this);
const uploadResults = [];
for (const group of uploadGroups) {
const pList = [];
for (const [src, dst] of group) {
pList.push(func(src, dst, null, false));
this.client.emit('upload', { source: src, destination: dst });
}
const groupResults = await Promise.all(pList);
for (const r of groupResults) {
uploadResults.push(r);
}
}
resolve(Promise.all(uploads));
})
.then((pList) => {
return Promise.all(pList);
})
.finally(() => {
removeTempListeners(this, listeners, uploadFiles);
});
return uploadResults;
} catch (e) {
throw this.fmtError(`${e.message} ${srcDir} to ${dstDir}`, 'uploadFiles', e.code);
} finally {
removeTempListeners(this, listeners, uploadFiles);
}
};

@@ -1283,5 +1301,5 @@

this.debugMsg(
`uploadDir: srcDir = ${srcDir} dstDir = ${dstDir} options = ${options}`
`uploadDir: srcDir = ${srcDir} dstDir = ${dstDir} options = ${options}`,
);
let { remoteDir, remoteStatus } = await getRemoteStatus(dstDir);
const { remoteDir, remoteStatus } = await getRemoteStatus(dstDir);
this.debugMsg(`uploadDir: remoteDir = ${remoteDir} remoteStatus = ${remoteStatus}`);

@@ -1299,7 +1317,7 @@ checkLocalStatus(srcDir);

dirEntries = dirEntries.filter((item) =>
options.filter(join(srcDir, item.name), item.isDirectory())
options.filter(join(srcDir, item.name), item.isDirectory()),
);
}
let dirUploads = dirEntries.filter((item) => item.isDirectory());
let fileUploads = dirEntries.filter((item) => !item.isDirectory());
const dirUploads = dirEntries.filter((item) => item.isDirectory());
const fileUploads = dirEntries.filter((item) => !item.isDirectory());
this.debugMsg(`uploadDir: dirUploads = ${dirUploads}`);

@@ -1309,4 +1327,4 @@ this.debugMsg(`uploadDir: fileUploads = ${fileUploads}`);

for (const d of dirUploads) {
let src = join(srcDir, d.name);
let dst = `${remoteDir}/${d.name}`;
const src = join(srcDir, d.name);
const dst = `${remoteDir}/${d.name}`;
await this.uploadDir(src, dst, options);

@@ -1340,8 +1358,8 @@ }

async downloadDir(srcDir, dstDir, options = { filter: null, useFastget: false }) {
const _getDownloadList = async (srcDir, filter) => {
const getDownloadList = async (srcDir, filter) => {
try {
let listing = await this.list(srcDir);
const listing = await this.list(srcDir);
if (filter) {
return listing.filter((item) =>
filter(`${srcDir}/${item.name}`, item.type === 'd')
filter(`${srcDir}/${item.name}`, item.type === 'd'),
);

@@ -1355,3 +1373,3 @@ }

const _prepareDestination = (dst) => {
const prepareDestination = (dst) => {
try {

@@ -1363,3 +1381,3 @@ const localCheck = haveLocalCreate(dst);

'prepareDestination',
localCheck.code
localCheck.code,
);

@@ -1372,3 +1390,3 @@ } else if (localCheck.status && !localCheck.type) {

'_prepareDestination',
errorCode.badPath
errorCode.badPath,
);

@@ -1383,21 +1401,36 @@ }

const _downloadFiles = (remotePath, localPath, fileList, useFastget) => {
let listeners;
return new Promise((resolve, reject) => {
listeners = addTempListeners(this, '_downloadFIles', reject);
let pList = [];
const downloadFiles = async (remotePath, localPath, fileList, useFastget) => {
let listeners = addTempListeners(this, 'downloadFIles');
try {
const downloadList = [];
for (const f of fileList) {
let src = `${remotePath}/${f.name}`;
let dst = join(localPath, f.name);
if (useFastget) {
pList.push(this.fastGet(src, dst, false));
} else {
pList.push(this.get(src, dst, false));
const src = `${remotePath}/${f.name}`;
const dst = join(localPath, f.name);
downloadList.push([src, dst]);
}
const downloadGroups = partition(downloadList, this.promiseLimit);
const func = useFastget ? this._fastGet.bind(this) : this.get.bind(this);
const downloadResults = [];
for (const group of downloadGroups) {
const pList = [];
for (const [src, dst] of group) {
pList.push(func(src, dst, null, false));
this.client.emit('download', { source: src, destination: dst });
}
this.client.emit('download', { source: src, destination: dst });
const groupResults = await Promise.all(pList);
for (const r of groupResults) {
downloadResults.push(r);
}
}
return resolve(Promise.all(pList));
}).finally(() => {
removeTempListeners(this, listeners, '_downloadFiles');
});
return downloadResults;
} catch (e) {
throw this.fmtError(
`${e.message} ${srcDir} to ${dstDir}`,
'downloadFiles',
e.code,
);
} finally {
removeTempListeners(this, listeners, 'downloadFiles');
}
};

@@ -1407,13 +1440,13 @@

haveConnection(this, 'downloadDir');
let downloadList = await _getDownloadList(srcDir, options.filter);
_prepareDestination(dstDir);
let fileDownloads = downloadList.filter((i) => i.type !== 'd');
const downloadList = await getDownloadList(srcDir, options.filter);
prepareDestination(dstDir);
const fileDownloads = downloadList.filter((i) => i.type !== 'd');
if (fileDownloads.length) {
await _downloadFiles(srcDir, dstDir, fileDownloads, options.useFastget);
await downloadFiles(srcDir, dstDir, fileDownloads, options.useFastget);
}
let dirDownloads = downloadList.filter((i) => i.type === 'd');
const dirDownloads = downloadList.filter((i) => i.type === 'd');
for (const d of dirDownloads) {
let src = `${srcDir}/${d.name}`;
let dst = join(dstDir, d.name);
await this.downloadDir(src, dst);
const src = `${srcDir}/${d.name}`;
const dst = join(dstDir, d.name);
await this.downloadDir(src, dst, options);
}

@@ -1429,3 +1462,2 @@ return `${srcDir} downloaded to ${dstDir}`;

/**
*
* Returns a read stream object. This is a low level method which will return a read stream

@@ -1440,3 +1472,2 @@ * connected to the remote file object specified as an argument. Client code is fully responsible

* @returns {Object} a read stream object
*
*/

@@ -1458,3 +1489,2 @@ createReadStream(remotePath, options) {

/**
*
* Create a write stream object connected to a file on the remote sftp server.

@@ -1469,3 +1499,2 @@ * This is a low level method which will return a write stream for the remote file specified

* @returns {Object} a stream object
*
*/

@@ -1497,3 +1526,2 @@ createWriteStream(remotePath, options) {

* @returns {String}.
*
*/

@@ -1528,3 +1556,3 @@ _rcopy(srcPath, dstPath) {

'rcopy',
errorCode.badPath
errorCode.badPath,
);

@@ -1541,3 +1569,3 @@ }

'rcopy',
errorCode.badPath
errorCode.badPath,
);

@@ -1544,0 +1572,0 @@ }

@@ -1,3 +0,3 @@

const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const { errorCode } = require('./constants');

@@ -43,3 +43,3 @@

function endListener(client, name, reject) {
function endListener(client, name) {
const fn = function () {

@@ -49,15 +49,7 @@ client.sftp = undefined;

// end event already handled - ignore
client.debugMsg(`${name} endListener - ignoring handled error`);
client.debugMsg(`${name} endListener - handled end event`);
return;
}
client.endHandled = true;
client.debugMsg(`${name} Unexpected end event - ignoring`);
// Don't reject/throw error, just log it and move on
// after invalidating the connection
// const err = new Error(`${name} Unexpected end event raised`);
// if (reject) {
// reject(err);
// } else {
// throw err;
// }
client.debugMsg(`${name} Unexpected end event`);
};

@@ -67,3 +59,3 @@ return fn;

function closeListener(client, name, reject) {
function closeListener(client, name) {
const fn = function () {

@@ -78,15 +70,7 @@ client.sftp = undefined;

// handled or expected close event - ignore
client.debugMsg(`${name} closeListener - ignoring handled error`);
client.debugMsg(`${name} closeListener - handled close event`);
return;
}
client.closeHandled = true;
client.debugMsg(`${name} Unexpected close event raised - ignoring`);
// Don't throw/reject on close events. Just invalidate the connection
// and move on.
// const err = new Error(`${name}: Unexpected close event raised`);
// if (reject) {
// reject(err);
// } else {
// throw err;
// }
client.debugMsg(`${name} Unexpected close event`);
};

@@ -98,4 +82,4 @@ return fn;

const listeners = {
end: endListener(client, name, reject),
close: closeListener(client, name, reject),
end: endListener(client, name),
close: closeListener(client, name),
error: errorListener(client, name, reject),

@@ -178,3 +162,3 @@ };

switch (err.errno) {
case -2:
case -2: {
return {

@@ -186,3 +170,4 @@ status: false,

};
case -13:
}
case -13: {
return {

@@ -194,3 +179,4 @@ status: false,

};
case -20:
}
case -20: {
return {

@@ -201,3 +187,4 @@ status: false,

};
default:
}
default: {
return {

@@ -208,2 +195,3 @@ status: false,

};
}
}

@@ -224,22 +212,18 @@ }

const { status, details, type } = haveLocalAccess(filePath, 'w');
if (!status && details === 'permission denied') {
//throw new Error(`Bad path: ${filePath}: permission denied`);
return {
status,
details,
type,
};
} else if (!status) {
if (!status) {
// filePath does not exist. Can we create it?
if (details === 'permission denied') {
// don't have permission
return {
status,
details,
type,
};
}
// to create it, parent must be directory and writeable
const dirPath = path.dirname(filePath);
const localCheck = haveLocalAccess(dirPath, 'w');
if (localCheck.status && localCheck.type !== 'd') {
//throw new Error(`Bad path: ${dirPath}: not a directory`);
if (!localCheck.status) {
// no access to parent directory
return {
status: false,
details: `${dirPath}: not a directory`,
type: null,
};
} else if (!localCheck.status) {
//throw new Error(`Bad path: ${dirPath}: ${localCheck.details}`);
return {
status: localCheck.status,

@@ -249,10 +233,17 @@ details: `${dirPath}: ${localCheck.details}`,

};
} else {
}
// exists, is it a directory?
if (localCheck.type !== 'd') {
return {
status: true,
details: 'access OK',
status: false,
details: `${dirPath}: not a directory`,
type: null,
code: 0,
};
}
return {
status: true,
details: 'access OK',
type: null,
code: 0,
};
}

@@ -304,4 +295,4 @@ return { status, details, type };

try {
if (isNaN(ms) || ms < 0) {
reject('Argument must be anumber >= 0');
if (Number.isNaN(Number.parseInt(ms)) || ms < 0) {
reject('Argument must be a number >= 0');
} else {

@@ -318,2 +309,15 @@ setTimeout(() => {

function partition(input, size) {
let output = [];
if (size < 1) {
throw new Error('Partition size must be greater than zero');
}
for (let i = 0; i < input.length; i += size) {
output[output.length] = input.slice(i, i + size);
}
return output;
}
module.exports = {

@@ -332,2 +336,3 @@ globalListener,

sleep,
partition,
};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc