@getbastionai/gatepost 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/package.json +30 -0
- package/src/blocklist.js +80 -0
- package/src/check.js +142 -0
- package/src/index.js +222 -0
- package/src/setup.js +75 -0
- package/src/typosquat.js +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bastion
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Gatepost
|
|
4
|
+
|
|
5
|
+
**Gatepost** wraps your package managers and scans every install for malicious packages — before they touch your machine.
|
|
6
|
+
|
|
7
|
+
Every install is checked against three layers of protection:
|
|
8
|
+
|
|
9
|
+
| Check | What it catches |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **Blocklist** | Known malicious packages by name |
|
|
12
|
+
| **Typosquat detection** | Lookalikes of popular packages (e.g. `lodahs` → `lodash`) |
|
|
13
|
+
| **CVE scanning** | Live vulnerability lookup via the [OSV.dev](https://osv.dev) API |
|
|
14
|
+
|
|
15
|
+
If a package is flagged, the install is blocked. If it's clean, Gatepost steps aside — zero friction, zero overhead.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Supported package managers
|
|
20
|
+
|
|
21
|
+
| Ecosystem | Managers |
|
|
22
|
+
|---|---|
|
|
23
|
+
| **Node / JS** | `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx` |
|
|
24
|
+
| **Python** | `pip`, `pip3`, `uv`, `poetry`, `pipx` |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install -g @getbastionai/gatepost
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it. Shell aliases are set up automatically — restart your terminal and every package manager command is protected.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
After install, use your package managers exactly as you normally would:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm install lodash
|
|
44
|
+
yarn add axios
|
|
45
|
+
pip install requests
|
|
46
|
+
bun add hono
|
|
47
|
+
npx create-react-app my-app
|
|
48
|
+
uv add fastapi
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Gatepost runs silently when everything is clean. Output only appears when something is flagged.
|
|
52
|
+
|
|
53
|
+
### Blocked install
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
gatepost: install blocked
|
|
57
|
+
|
|
58
|
+
blocked event-stream Known malicious package
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The install exits with code 1. Nothing was installed.
|
|
62
|
+
|
|
63
|
+
### Warning (install proceeds)
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
gatepost: warning
|
|
67
|
+
|
|
68
|
+
warn lodahs Possible typosquat of "lodash"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Warnings are shown but the install is not blocked — you decide.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Manual check
|
|
76
|
+
|
|
77
|
+
Scan packages without installing them:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
gatepost check express axios lodash
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
ok express
|
|
85
|
+
ok axios
|
|
86
|
+
ok lodash
|
|
87
|
+
|
|
88
|
+
All packages look clean.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Commands
|
|
94
|
+
|
|
95
|
+
| Command | Description |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `gatepost setup` | Add shell aliases (run once after install) |
|
|
98
|
+
| `gatepost remove` | Remove shell aliases |
|
|
99
|
+
| `gatepost check <pkg...>` | Scan packages without installing |
|
|
100
|
+
| `gatepost <manager> [args]` | Run any manager with protection |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## How it works
|
|
105
|
+
|
|
106
|
+
1. Shell aliases silently redirect `npm install foo` → `gatepost npm install foo`
|
|
107
|
+
2. Gatepost extracts package names from the command arguments
|
|
108
|
+
3. Three checks run in parallel: blocklist lookup, typosquat similarity, OSV CVE query
|
|
109
|
+
4. Blocked packages exit with code 1 — nothing installs
|
|
110
|
+
5. Warnings print to stderr but allow the install to continue
|
|
111
|
+
6. Clean packages pass straight through to the real package manager
|
|
112
|
+
|
|
113
|
+
If the OSV network request fails (offline or timeout), Gatepost warns and proceeds — it never blocks a legitimate workflow.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Uninstall
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
gatepost remove
|
|
121
|
+
npm uninstall -g @getbastionai/gatepost
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Requirements
|
|
127
|
+
|
|
128
|
+
- Node.js 16+
|
|
129
|
+
- npm (for global install)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getbastionai/gatepost",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Wraps npm, npx, yarn, pnpm, and pnpx to block malicious packages in real-time",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"postinstall": "node ./src/index.js setup"
|
|
7
|
+
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"gatepost": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"security",
|
|
16
|
+
"supply-chain",
|
|
17
|
+
"npm",
|
|
18
|
+
"malware",
|
|
19
|
+
"package-manager",
|
|
20
|
+
"typosquatting"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=16"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/GetBastion/gatepost.git"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/blocklist.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Known malicious packages sourced from npm security advisories,
|
|
4
|
+
// GitHub Security Advisories, and public threat intelligence reports.
|
|
5
|
+
const BLOCKLIST = new Set([
|
|
6
|
+
// Compromised/backdoored packages
|
|
7
|
+
'event-stream',
|
|
8
|
+
'flatmap-stream',
|
|
9
|
+
'node-ipc',
|
|
10
|
+
'ua-parser-js',
|
|
11
|
+
'coa',
|
|
12
|
+
'rc',
|
|
13
|
+
'colors',
|
|
14
|
+
'faker',
|
|
15
|
+
|
|
16
|
+
// Typosquatting - targeting popular packages
|
|
17
|
+
'crossenv',
|
|
18
|
+
'cross-env.js',
|
|
19
|
+
'ffmmpeg',
|
|
20
|
+
'babelcli',
|
|
21
|
+
'nodecaffe',
|
|
22
|
+
'nodefabric',
|
|
23
|
+
'node-fabric',
|
|
24
|
+
'nodeffmpeg',
|
|
25
|
+
'nodemysql',
|
|
26
|
+
'node-opencv',
|
|
27
|
+
'node-openssl',
|
|
28
|
+
'node-os',
|
|
29
|
+
'node-paint',
|
|
30
|
+
'node-pentest',
|
|
31
|
+
'node-sqlite',
|
|
32
|
+
'node-tkinter',
|
|
33
|
+
'nodecrypto',
|
|
34
|
+
'nodeftp',
|
|
35
|
+
'nodemailer-js',
|
|
36
|
+
'nodemailer.js',
|
|
37
|
+
'socketio',
|
|
38
|
+
'socket.io.js',
|
|
39
|
+
'discordie',
|
|
40
|
+
'mongose',
|
|
41
|
+
'mssql-node',
|
|
42
|
+
'mysqljs',
|
|
43
|
+
'node-sass-middleware',
|
|
44
|
+
'gruntcli',
|
|
45
|
+
'jquey',
|
|
46
|
+
'jquery.js',
|
|
47
|
+
'd3.js',
|
|
48
|
+
'require',
|
|
49
|
+
'loadash',
|
|
50
|
+
'momnet',
|
|
51
|
+
'expres',
|
|
52
|
+
'expresss',
|
|
53
|
+
'reakt',
|
|
54
|
+
'reactt',
|
|
55
|
+
'vue.js',
|
|
56
|
+
'vuejs',
|
|
57
|
+
'angularjs',
|
|
58
|
+
'lodahs',
|
|
59
|
+
'lodash.js',
|
|
60
|
+
'requets',
|
|
61
|
+
'requset',
|
|
62
|
+
'underscore.js',
|
|
63
|
+
'underscorejs',
|
|
64
|
+
'webpak',
|
|
65
|
+
'webpackk',
|
|
66
|
+
'axio',
|
|
67
|
+
'axxios',
|
|
68
|
+
'dotenv.js',
|
|
69
|
+
|
|
70
|
+
// Known credential stealers (public reports)
|
|
71
|
+
'electron-native-notify',
|
|
72
|
+
'getcookies',
|
|
73
|
+
'http-fetch-client',
|
|
74
|
+
'aws-sdk-config',
|
|
75
|
+
'node-browserify',
|
|
76
|
+
'nodecookies',
|
|
77
|
+
'xss-payloads',
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
module.exports = { BLOCKLIST }
|
package/src/check.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const https = require('https')
|
|
4
|
+
const { BLOCKLIST } = require('./blocklist')
|
|
5
|
+
const { POPULAR_PACKAGES } = require('./typosquat')
|
|
6
|
+
|
|
7
|
+
// Levenshtein distance — measures edit distance between two strings
|
|
8
|
+
function levenshtein(a, b) {
|
|
9
|
+
const m = a.length, n = b.length
|
|
10
|
+
const dp = []
|
|
11
|
+
for (let i = 0; i <= m; i++) {
|
|
12
|
+
dp[i] = [i]
|
|
13
|
+
for (let j = 1; j <= n; j++) {
|
|
14
|
+
dp[i][j] = i === 0
|
|
15
|
+
? j
|
|
16
|
+
: a[i - 1] === b[j - 1]
|
|
17
|
+
? dp[i - 1][j - 1]
|
|
18
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return dp[m][n]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Parse the canonical package name from an install arg
|
|
25
|
+
// Handles: lodash, lodash@4.x, @types/node, @types/node@18.0.0
|
|
26
|
+
function parsePkgName(arg) {
|
|
27
|
+
if (arg.startsWith('@')) {
|
|
28
|
+
const match = arg.match(/^(@[^/]+\/[^@]+)/)
|
|
29
|
+
return match ? match[1] : null
|
|
30
|
+
}
|
|
31
|
+
const name = arg.split('@')[0]
|
|
32
|
+
return name || null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Skip non-package args (URLs, file paths, git refs)
|
|
36
|
+
function isPackageName(arg) {
|
|
37
|
+
if (!arg || arg.startsWith('-')) return false
|
|
38
|
+
if (arg.startsWith('git+') || arg.startsWith('github:') ||
|
|
39
|
+
arg.startsWith('gitlab:') || arg.startsWith('bitbucket:') ||
|
|
40
|
+
arg.startsWith('file:') || arg.startsWith('http') ||
|
|
41
|
+
arg.startsWith('.') || arg.startsWith('/')) return false
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check for typosquatting against popular packages
|
|
46
|
+
function checkTyposquat(pkgName, ecosystem = 'npm') {
|
|
47
|
+
const { POPULAR_PYTHON_PACKAGES } = require('./typosquat')
|
|
48
|
+
const pool = ecosystem === 'PyPI' ? POPULAR_PYTHON_PACKAGES : POPULAR_PACKAGES
|
|
49
|
+
|
|
50
|
+
// Use the local name for scoped npm packages: @types/node -> node
|
|
51
|
+
const localName = pkgName.startsWith('@')
|
|
52
|
+
? pkgName.split('/')[1] || ''
|
|
53
|
+
: pkgName
|
|
54
|
+
|
|
55
|
+
const lower = localName.toLowerCase()
|
|
56
|
+
if (lower.length < 4) return null
|
|
57
|
+
|
|
58
|
+
for (const popular of pool) {
|
|
59
|
+
if (lower === popular.toLowerCase()) return null // exact match, not a typosquat
|
|
60
|
+
const dist = levenshtein(lower, popular.toLowerCase())
|
|
61
|
+
if (dist >= 1 && dist <= 2) return popular
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Query the OSV.dev API for known vulnerabilities
|
|
67
|
+
function queryOSV(pkgName, ecosystem = 'npm') {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const body = JSON.stringify({
|
|
70
|
+
package: { name: pkgName, ecosystem },
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const req = https.request({
|
|
74
|
+
hostname: 'api.osv.dev',
|
|
75
|
+
path: '/v1/query',
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'Content-Length': Buffer.byteLength(body),
|
|
80
|
+
},
|
|
81
|
+
timeout: 5000,
|
|
82
|
+
}, (res) => {
|
|
83
|
+
let data = ''
|
|
84
|
+
res.on('data', chunk => { data += chunk })
|
|
85
|
+
res.on('end', () => {
|
|
86
|
+
try {
|
|
87
|
+
const json = JSON.parse(data)
|
|
88
|
+
resolve(json.vulns && json.vulns.length > 0 ? json.vulns : null)
|
|
89
|
+
} catch {
|
|
90
|
+
resolve(null)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
req.on('error', () => resolve(null))
|
|
96
|
+
req.on('timeout', () => { req.destroy(); resolve(null) })
|
|
97
|
+
req.write(body)
|
|
98
|
+
req.end()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check a single package — returns { pkg, issues[] }
|
|
103
|
+
async function checkPackage(arg, ecosystem = 'npm') {
|
|
104
|
+
const pkgName = ecosystem === 'npm' ? parsePkgName(arg) : arg.trim()
|
|
105
|
+
if (!pkgName) return { pkg: arg, issues: [] }
|
|
106
|
+
|
|
107
|
+
const issues = []
|
|
108
|
+
|
|
109
|
+
// 1. Hard blocklist
|
|
110
|
+
if (BLOCKLIST.has(pkgName)) {
|
|
111
|
+
issues.push({ type: 'blocklist', severity: 'block', message: 'Known malicious package' })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Typosquatting
|
|
115
|
+
const similar = checkTyposquat(pkgName, ecosystem)
|
|
116
|
+
if (similar) {
|
|
117
|
+
issues.push({ type: 'typosquat', severity: 'warn', message: `Possible typosquat of "${similar}"` })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. OSV vulnerability database
|
|
121
|
+
const vulns = await queryOSV(pkgName, ecosystem)
|
|
122
|
+
if (vulns) {
|
|
123
|
+
const count = vulns.length
|
|
124
|
+
issues.push({
|
|
125
|
+
type: 'vuln',
|
|
126
|
+
severity: 'warn',
|
|
127
|
+
message: `${count} known vulnerabilit${count === 1 ? 'y' : 'ies'} found in OSV database`,
|
|
128
|
+
vulns,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { pkg: pkgName, issues }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check multiple packages in parallel
|
|
136
|
+
async function checkPackages(args, ecosystem = 'npm') {
|
|
137
|
+
const packageArgs = args.filter(isPackageName)
|
|
138
|
+
if (packageArgs.length === 0) return []
|
|
139
|
+
return Promise.all(packageArgs.map(a => checkPackage(a, ecosystem)))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { checkPackages, parsePkgName, isPackageName }
|
package/src/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('child_process')
|
|
5
|
+
const { checkPackages } = require('./check')
|
|
6
|
+
const { setup, remove } = require('./setup')
|
|
7
|
+
|
|
8
|
+
const MANAGERS = ['npm', 'npx', 'yarn', 'pnpm', 'pnpx', 'bun', 'bunx', 'pip', 'pip3', 'uv', 'poetry', 'pipx']
|
|
9
|
+
|
|
10
|
+
// Ecosystem per manager — used for OSV API queries
|
|
11
|
+
const ECOSYSTEMS = {
|
|
12
|
+
npm: 'npm', npx: 'npm', yarn: 'npm', pnpm: 'npm', pnpx: 'npm',
|
|
13
|
+
bun: 'npm', bunx: 'npm',
|
|
14
|
+
pip: 'PyPI', pip3: 'PyPI', uv: 'PyPI', poetry: 'PyPI', pipx: 'PyPI',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Subcommands that trigger a package install (null = the command itself is the package)
|
|
18
|
+
const INSTALL_SUBCMDS = {
|
|
19
|
+
npm: ['install', 'i', 'add', 'ci'],
|
|
20
|
+
yarn: ['add'],
|
|
21
|
+
pnpm: ['add', 'install', 'i'],
|
|
22
|
+
npx: null,
|
|
23
|
+
pnpx: null,
|
|
24
|
+
bun: ['add', 'install', 'i'],
|
|
25
|
+
bunx: null,
|
|
26
|
+
pip: ['install'],
|
|
27
|
+
pip3: ['install'],
|
|
28
|
+
uv: ['add', 'pip install'], // handled specially below
|
|
29
|
+
poetry: ['add'],
|
|
30
|
+
pipx: ['install', 'run'],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ANSI colors
|
|
34
|
+
const c = {
|
|
35
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
36
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
37
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
38
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
39
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printHelp() {
|
|
43
|
+
console.log(`
|
|
44
|
+
${c.bold('gatepost')} — secure your package installs
|
|
45
|
+
|
|
46
|
+
${c.bold('Usage:')}
|
|
47
|
+
gatepost setup Install shell aliases (run once)
|
|
48
|
+
gatepost remove Remove shell aliases
|
|
49
|
+
gatepost check <pkg...> Manually check packages
|
|
50
|
+
gatepost <manager> [args] Run a package manager with protection
|
|
51
|
+
|
|
52
|
+
${c.bold('Examples:')}
|
|
53
|
+
gatepost setup
|
|
54
|
+
gatepost npm install lodash
|
|
55
|
+
gatepost check express axios
|
|
56
|
+
|
|
57
|
+
${c.bold('Supported managers:')}
|
|
58
|
+
npm, npx, yarn, pnpm, pnpx, bun, bunx
|
|
59
|
+
pip, pip3, uv, poetry, pipx
|
|
60
|
+
|
|
61
|
+
After running ${c.bold('gatepost setup')}, your package managers are
|
|
62
|
+
automatically protected — no need to type "gatepost" each time.
|
|
63
|
+
`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Run the real package manager, stripping our shim dir from PATH to avoid recursion
|
|
67
|
+
function passThrough(manager, args) {
|
|
68
|
+
const result = spawnSync(manager, args, {
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
// Remove ~/.gatepost from PATH if present (shim recursion guard)
|
|
73
|
+
PATH: (process.env.PATH || '').split(':')
|
|
74
|
+
.filter(p => !p.includes('.gatepost'))
|
|
75
|
+
.join(':'),
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
process.exit(result.status ?? 0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract package names from install args (skip flags and subcommand)
|
|
82
|
+
function extractPackages(manager, args) {
|
|
83
|
+
// Single-package executors — first non-flag arg is the package
|
|
84
|
+
if (['npx', 'pnpx', 'bunx'].includes(manager)) {
|
|
85
|
+
const pkg = args.find(a => !a.startsWith('-'))
|
|
86
|
+
return pkg ? [pkg] : []
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// uv has two install forms: `uv add pkg` and `uv pip install pkg`
|
|
90
|
+
if (manager === 'uv') {
|
|
91
|
+
const rest = args[0] === 'pip' ? args.slice(2) : args.slice(1)
|
|
92
|
+
return rest.filter(a => !a.startsWith('-')).map(stripPythonVersion)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// pip/pip3/poetry/pipx — skip subcommand, grab non-flag args
|
|
96
|
+
if (['pip', 'pip3', 'poetry', 'pipx'].includes(manager)) {
|
|
97
|
+
const rest = args.slice(1)
|
|
98
|
+
return rest.filter(a => !a.startsWith('-')).map(stripPythonVersion)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// npm/yarn/pnpm/bun — skip subcommand, grab non-flag args
|
|
102
|
+
const rest = args.slice(1)
|
|
103
|
+
return rest.filter(a => !a.startsWith('-'))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Strip Python version specifiers: requests==2.28.0 -> requests, flask[async] -> flask
|
|
107
|
+
function stripPythonVersion(arg) {
|
|
108
|
+
return arg.replace(/[=<>!~^].*/,'').replace(/\[.*\]/, '').trim()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function runWrapped(manager, args) {
|
|
112
|
+
const installSubcmds = INSTALL_SUBCMDS[manager]
|
|
113
|
+
const subCmd = args[0]
|
|
114
|
+
|
|
115
|
+
// Not an install command — pass straight through
|
|
116
|
+
const isInstall = installSubcmds === null
|
|
117
|
+
? true
|
|
118
|
+
: installSubcmds.includes(subCmd)
|
|
119
|
+
|
|
120
|
+
if (!isInstall) {
|
|
121
|
+
return passThrough(manager, args)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pkgs = extractPackages(manager, args)
|
|
125
|
+
|
|
126
|
+
// No explicit packages — installing from lockfile, pass through
|
|
127
|
+
if (pkgs.length === 0) {
|
|
128
|
+
return passThrough(manager, args)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ecosystem = ECOSYSTEMS[manager] || 'npm'
|
|
132
|
+
process.stderr.write(c.dim(`gatepost: checking ${pkgs.join(', ')}...\n`))
|
|
133
|
+
|
|
134
|
+
let results
|
|
135
|
+
try {
|
|
136
|
+
results = await checkPackages(pkgs, ecosystem)
|
|
137
|
+
} catch {
|
|
138
|
+
// Network failure — warn and proceed
|
|
139
|
+
process.stderr.write(c.yellow('gatepost: security check failed (network error), proceeding anyway\n'))
|
|
140
|
+
return passThrough(manager, args)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const blocked = results.filter(r => r.issues.some(i => i.severity === 'block'))
|
|
144
|
+
const warned = results.filter(r => r.issues.some(i => i.severity === 'warn') && !blocked.find(b => b.pkg === r.pkg))
|
|
145
|
+
|
|
146
|
+
if (blocked.length > 0) {
|
|
147
|
+
console.error(c.red(c.bold('\ngatepost: install blocked\n')))
|
|
148
|
+
for (const r of blocked) {
|
|
149
|
+
for (const issue of r.issues) {
|
|
150
|
+
console.error(` ${c.red('blocked')} ${c.bold(r.pkg)} ${issue.message}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
console.error('')
|
|
154
|
+
process.exit(1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (warned.length > 0) {
|
|
158
|
+
console.error(c.yellow(c.bold('\ngatepost: warning\n')))
|
|
159
|
+
for (const r of warned) {
|
|
160
|
+
for (const issue of r.issues) {
|
|
161
|
+
console.error(` ${c.yellow('warn')} ${c.bold(r.pkg)} ${issue.message}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
console.error('')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
passThrough(manager, args)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function manualCheck(pkgs) {
|
|
171
|
+
if (pkgs.length === 0) {
|
|
172
|
+
console.error('Usage: gatepost check <package...>')
|
|
173
|
+
process.exit(1)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(c.dim(`Checking ${pkgs.length} package(s) against blocklist, typosquatting, and OSV database...\n`))
|
|
177
|
+
|
|
178
|
+
const results = await checkPackages(pkgs)
|
|
179
|
+
|
|
180
|
+
let allClear = true
|
|
181
|
+
for (const r of results) {
|
|
182
|
+
if (r.issues.length === 0) {
|
|
183
|
+
console.log(` ${c.green('ok')} ${c.bold(r.pkg)}`)
|
|
184
|
+
} else {
|
|
185
|
+
allClear = false
|
|
186
|
+
for (const issue of r.issues) {
|
|
187
|
+
const label = issue.severity === 'block' ? c.red('blocked') : c.yellow('warn')
|
|
188
|
+
console.log(` ${label} ${c.bold(r.pkg)} ${issue.message}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (allClear) {
|
|
194
|
+
console.log(c.green('\nAll packages look clean.'))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- CLI router ---
|
|
199
|
+
const args = process.argv.slice(2)
|
|
200
|
+
const command = args[0]
|
|
201
|
+
|
|
202
|
+
if (!command || command === '--help' || command === '-h') {
|
|
203
|
+
printHelp()
|
|
204
|
+
} else if (command === 'setup') {
|
|
205
|
+
setup()
|
|
206
|
+
} else if (command === 'remove' || command === 'uninstall') {
|
|
207
|
+
remove()
|
|
208
|
+
} else if (command === 'check') {
|
|
209
|
+
manualCheck(args.slice(1)).catch(err => {
|
|
210
|
+
console.error('Error:', err.message)
|
|
211
|
+
process.exit(1)
|
|
212
|
+
})
|
|
213
|
+
} else if (MANAGERS.includes(command)) {
|
|
214
|
+
runWrapped(command, args.slice(1)).catch(err => {
|
|
215
|
+
console.error('gatepost error:', err.message)
|
|
216
|
+
process.exit(1)
|
|
217
|
+
})
|
|
218
|
+
} else {
|
|
219
|
+
console.error(`Unknown command: ${command}`)
|
|
220
|
+
printHelp()
|
|
221
|
+
process.exit(1)
|
|
222
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
|
|
7
|
+
const MANAGERS = ['npm', 'npx', 'yarn', 'pnpm', 'pnpx', 'bun', 'bunx', 'pip', 'pip3', 'uv', 'poetry', 'pipx']
|
|
8
|
+
const MARKER_START = '# gatepost-start'
|
|
9
|
+
const MARKER_END = '# gatepost-end'
|
|
10
|
+
|
|
11
|
+
function buildAliasBlock() {
|
|
12
|
+
const aliases = MANAGERS.map(m => `alias ${m}='gatepost ${m}'`).join('\n')
|
|
13
|
+
return `\n${MARKER_START}\n${aliases}\n${MARKER_END}\n`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getShellConfigs() {
|
|
17
|
+
const home = os.homedir()
|
|
18
|
+
return [
|
|
19
|
+
path.join(home, '.zshrc'),
|
|
20
|
+
path.join(home, '.bashrc'),
|
|
21
|
+
path.join(home, '.bash_profile'),
|
|
22
|
+
path.join(home, '.profile'),
|
|
23
|
+
].filter(f => fs.existsSync(f))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setup() {
|
|
27
|
+
const block = buildAliasBlock()
|
|
28
|
+
const configs = getShellConfigs()
|
|
29
|
+
|
|
30
|
+
if (configs.length === 0) {
|
|
31
|
+
console.log('No shell config file found. Add these aliases manually:\n')
|
|
32
|
+
console.log(block)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let updated = 0
|
|
37
|
+
for (const config of configs) {
|
|
38
|
+
const contents = fs.readFileSync(config, 'utf8')
|
|
39
|
+
if (contents.includes(MARKER_START)) {
|
|
40
|
+
console.log(` Already configured: ${config}`)
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
fs.appendFileSync(config, block)
|
|
44
|
+
console.log(` Updated: ${config}`)
|
|
45
|
+
updated++
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (updated > 0) {
|
|
49
|
+
console.log('\nGatepost is set up. Restart your terminal or run:')
|
|
50
|
+
console.log(' source ~/.zshrc\n')
|
|
51
|
+
console.log('After that, npm, npx, yarn, pnpm, and pnpx will be protected automatically.')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function remove() {
|
|
56
|
+
const configs = getShellConfigs()
|
|
57
|
+
const re = new RegExp(`\\n${MARKER_START}[\\s\\S]*?${MARKER_END}\\n`, 'g')
|
|
58
|
+
|
|
59
|
+
let removed = 0
|
|
60
|
+
for (const config of configs) {
|
|
61
|
+
const contents = fs.readFileSync(config, 'utf8')
|
|
62
|
+
if (!contents.includes(MARKER_START)) continue
|
|
63
|
+
fs.writeFileSync(config, contents.replace(re, '\n'))
|
|
64
|
+
console.log(` Removed aliases from: ${config}`)
|
|
65
|
+
removed++
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (removed === 0) {
|
|
69
|
+
console.log('No Gatepost aliases found to remove.')
|
|
70
|
+
} else {
|
|
71
|
+
console.log('\nGatepost removed. Restart your terminal to apply.')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { setup, remove }
|
package/src/typosquat.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Top npm packages by download count — used for typosquatting detection.
|
|
4
|
+
// If a package name is within edit distance 1-2 of one of these, it's flagged.
|
|
5
|
+
const POPULAR_PACKAGES = [
|
|
6
|
+
'lodash', 'express', 'react', 'chalk', 'commander', 'axios', 'moment',
|
|
7
|
+
'webpack', 'babel', 'typescript', 'eslint', 'prettier', 'jest', 'mocha',
|
|
8
|
+
'dotenv', 'mongoose', 'sequelize', 'socket.io', 'nodemailer', 'passport',
|
|
9
|
+
'bcrypt', 'jsonwebtoken', 'cors', 'helmet', 'morgan', 'body-parser',
|
|
10
|
+
'uuid', 'underscore', 'async', 'request', 'superagent', 'got', 'node-fetch',
|
|
11
|
+
'inquirer', 'yargs', 'minimist', 'glob', 'rimraf', 'mkdirp', 'fs-extra',
|
|
12
|
+
'path', 'events', 'stream', 'buffer', 'util', 'crypto', 'http', 'https',
|
|
13
|
+
'vue', 'angular', 'svelte', 'next', 'nuxt', 'gatsby',
|
|
14
|
+
'react-dom', 'react-router', 'redux', 'mobx', 'zustand',
|
|
15
|
+
'vite', 'rollup', 'parcel', 'esbuild',
|
|
16
|
+
'prettier', 'husky', 'lint-staged',
|
|
17
|
+
'prisma', 'typeorm', 'knex', 'redis', 'ioredis',
|
|
18
|
+
'sharp', 'jimp', 'multer', 'formidable',
|
|
19
|
+
'ws', 'uws', 'fastify', 'koa', 'hapi', 'restify',
|
|
20
|
+
'pm2', 'nodemon', 'concurrently',
|
|
21
|
+
'cheerio', 'puppeteer', 'playwright',
|
|
22
|
+
'aws-sdk', 'firebase', 'supabase',
|
|
23
|
+
'tailwindcss', 'postcss', 'autoprefixer',
|
|
24
|
+
'zod', 'joi', 'yup', 'ajv',
|
|
25
|
+
'date-fns', 'luxon', 'dayjs',
|
|
26
|
+
'ramda', 'immer', 'immutable',
|
|
27
|
+
'rxjs', 'bluebird', 'p-limit',
|
|
28
|
+
'semver', 'acorn', 'terser',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
// Top PyPI packages by download count
|
|
32
|
+
const POPULAR_PYTHON_PACKAGES = [
|
|
33
|
+
'requests', 'numpy', 'pandas', 'flask', 'django', 'fastapi', 'sqlalchemy',
|
|
34
|
+
'pytest', 'boto3', 'pydantic', 'celery', 'redis', 'pillow', 'scipy',
|
|
35
|
+
'matplotlib', 'tensorflow', 'torch', 'scikit-learn', 'transformers',
|
|
36
|
+
'cryptography', 'paramiko', 'fabric', 'ansible', 'click', 'typer',
|
|
37
|
+
'httpx', 'aiohttp', 'uvicorn', 'gunicorn', 'starlette', 'fastapi',
|
|
38
|
+
'alembic', 'marshmallow', 'attrs', 'pyyaml', 'toml', 'dotenv',
|
|
39
|
+
'python-dotenv', 'rich', 'loguru', 'arrow', 'pendulum', 'dateutil',
|
|
40
|
+
'six', 'packaging', 'setuptools', 'wheel', 'pip', 'virtualenv',
|
|
41
|
+
'black', 'mypy', 'flake8', 'pylint', 'isort', 'bandit',
|
|
42
|
+
'docker', 'kubernetes', 'google-cloud', 'azure', 'openai', 'anthropic',
|
|
43
|
+
'langchain', 'opentelemetry', 'prometheus-client', 'psutil',
|
|
44
|
+
'psycopg2', 'pymongo', 'motor', 'elasticsearch', 'stripe',
|
|
45
|
+
'twilio', 'sendgrid', 'jinja2', 'werkzeug', 'itsdangerous',
|
|
46
|
+
'lxml', 'beautifulsoup4', 'selenium', 'playwright', 'scrapy',
|
|
47
|
+
'PyJWT', 'bcrypt', 'passlib', 'authlib',
|
|
48
|
+
'tqdm', 'tabulate', 'openpyxl', 'xlrd',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
module.exports = { POPULAR_PACKAGES, POPULAR_PYTHON_PACKAGES }
|