trust-npm 0.1.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 +154 -0
- package/dist/cli.js +58 -0
- package/dist/commands/approve.js +18 -0
- package/dist/commands/init.js +105 -0
- package/dist/commands/install.js +66 -0
- package/dist/commands/status.js +54 -0
- package/dist/core/lockfile.js +48 -0
- package/dist/core/npm.js +15 -0
- package/dist/core/project.js +22 -0
- package/dist/core/registry.js +28 -0
- package/dist/core/risk.js +86 -0
- package/dist/core/trustStore.js +57 -0
- package/dist/types.js +2 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 trust-npm contributors
|
|
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,154 @@
|
|
|
1
|
+
# trust-npm
|
|
2
|
+
|
|
3
|
+
`trust-npm` is a production-oriented npm wrapper that blocks unknown dependencies by default and requires explicit approvals.
|
|
4
|
+
|
|
5
|
+
It uses your existing `package-lock.json` as a trust baseline and intercepts `npm install` before allowing package installation.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Default-deny installs for unknown packages
|
|
10
|
+
- Trust baseline bootstrapped from `package-lock.json`
|
|
11
|
+
- Risk scoring for unknown packages (age, downloads, repo, naming pattern)
|
|
12
|
+
- Explicit approvals via `trust-npm approve`
|
|
13
|
+
- Cross-platform npm passthrough using `child_process.spawn`
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run build
|
|
20
|
+
npm link
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then use:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
trust-npm --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## One-command project setup
|
|
30
|
+
|
|
31
|
+
If you want npm commands to go through `trust-npm` automatically:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
trust-npm init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will:
|
|
38
|
+
|
|
39
|
+
- create/update `.trust-npm.json` from `package-lock.json`
|
|
40
|
+
- persist alias `npm -> trust-npm` in your shell profile
|
|
41
|
+
|
|
42
|
+
You can force shell target:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
trust-npm init --shell powershell
|
|
46
|
+
trust-npm init --shell bash
|
|
47
|
+
trust-npm init --shell zsh
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
### `trust-npm init`
|
|
53
|
+
|
|
54
|
+
Initializes `.trust-npm.json` using lockfile dependencies and sets a persistent shell alias (`npm -> trust-npm`).
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
trust-npm init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Skip automatic alias setup:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
trust-npm init --skip-alias
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Optional alias instructions:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
trust-npm init --print-shell-alias
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `trust-npm install <package...>`
|
|
73
|
+
|
|
74
|
+
Intercepts install requests and blocks unknown packages until explicitly approved.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
trust-npm install lodash
|
|
78
|
+
trust-npm install react -D
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If blocked:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
❌ BLOCKED (high risk): fast-ultra-db-kit
|
|
85
|
+
Reason:
|
|
86
|
+
- Published 2 day(s) ago
|
|
87
|
+
- 12 weekly downloads
|
|
88
|
+
- Missing repository field
|
|
89
|
+
- Risk score: 90/50
|
|
90
|
+
|
|
91
|
+
Run:
|
|
92
|
+
trust-npm approve fast-ultra-db-kit
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `trust-npm approve <package...>`
|
|
96
|
+
|
|
97
|
+
Adds packages to trusted store:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
trust-npm approve lodash
|
|
101
|
+
trust-npm approve react react-dom
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `trust-npm status`
|
|
105
|
+
|
|
106
|
+
Shows high-level trust status for the current project:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
trust-npm status
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Trust Store Format
|
|
113
|
+
|
|
114
|
+
`.trust-npm.json`:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"version": 1,
|
|
119
|
+
"createdAt": "2026-04-04T10:00:00.000Z",
|
|
120
|
+
"updatedAt": "2026-04-04T10:05:00.000Z",
|
|
121
|
+
"trustedPackages": {
|
|
122
|
+
"lodash": {
|
|
123
|
+
"source": "lockfile",
|
|
124
|
+
"approvedAt": "2026-04-04T10:00:00.000Z"
|
|
125
|
+
},
|
|
126
|
+
"fast-ultra-db-kit": {
|
|
127
|
+
"source": "manual",
|
|
128
|
+
"approvedAt": "2026-04-04T10:05:00.000Z"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"riskThreshold": 50
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Security Model
|
|
136
|
+
|
|
137
|
+
- Unknown package install attempts are blocked.
|
|
138
|
+
- High-risk unknown packages are called out with detailed risk factors.
|
|
139
|
+
- Even low-risk unknown packages are still blocked until approved.
|
|
140
|
+
- No silent approvals.
|
|
141
|
+
|
|
142
|
+
## Notes
|
|
143
|
+
|
|
144
|
+
- `trust-npm install` forwards to real `npm install` only when checks pass.
|
|
145
|
+
- If you run plain `npm install`, trust checks are bypassed. Use shell aliasing if desired.
|
|
146
|
+
- Network access to npm registry is required for risk analysis on unknown packages.
|
|
147
|
+
|
|
148
|
+
## Future Extensions
|
|
149
|
+
|
|
150
|
+
The structure is prepared for:
|
|
151
|
+
|
|
152
|
+
- additional package manager adapters (for example, `pip`)
|
|
153
|
+
- runtime import/require protection
|
|
154
|
+
- CI/CD enforcement mode
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const init_1 = require("./commands/init");
|
|
6
|
+
const install_1 = require("./commands/install");
|
|
7
|
+
const approve_1 = require("./commands/approve");
|
|
8
|
+
const status_1 = require("./commands/status");
|
|
9
|
+
const program = new commander_1.Command();
|
|
10
|
+
program
|
|
11
|
+
.name("trust-npm")
|
|
12
|
+
.description("Secure wrapper around npm install with trust baseline checks")
|
|
13
|
+
.version("0.1.0");
|
|
14
|
+
program
|
|
15
|
+
.command("init")
|
|
16
|
+
.description("Create .trust-npm.json and auto-alias npm -> trust-npm")
|
|
17
|
+
.option("--print-shell-alias", "Print alias instructions")
|
|
18
|
+
.option("--skip-alias", "Skip automatic shell alias setup")
|
|
19
|
+
.option("--shell <shell>", "Alias target shell (powershell|bash|zsh)")
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
await (0, init_1.runInit)(options);
|
|
22
|
+
});
|
|
23
|
+
program
|
|
24
|
+
.command("install [packages...]")
|
|
25
|
+
.description("Intercept npm install and block unknown/untrusted packages")
|
|
26
|
+
.allowUnknownOption(true)
|
|
27
|
+
.action(async () => {
|
|
28
|
+
const rawInstallArgs = getRawInstallArgs(process.argv);
|
|
29
|
+
await (0, install_1.runInstall)(rawInstallArgs);
|
|
30
|
+
});
|
|
31
|
+
program
|
|
32
|
+
.command("approve <packages...>")
|
|
33
|
+
.description("Approve one or more packages into .trust-npm.json")
|
|
34
|
+
.action(async (packages) => {
|
|
35
|
+
await (0, approve_1.runApprove)(packages);
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command("status")
|
|
39
|
+
.description("Show trusted vs untrusted dependency status")
|
|
40
|
+
.action(async () => {
|
|
41
|
+
await (0, status_1.runStatus)();
|
|
42
|
+
});
|
|
43
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
console.error(`trust-npm error: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.error("trust-npm error: unknown failure");
|
|
49
|
+
}
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
});
|
|
52
|
+
function getRawInstallArgs(argv) {
|
|
53
|
+
const installIndex = argv.findIndex((token) => token === "install");
|
|
54
|
+
if (installIndex < 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return argv.slice(installIndex + 1);
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runApprove = runApprove;
|
|
4
|
+
const project_1 = require("../core/project");
|
|
5
|
+
const trustStore_1 = require("../core/trustStore");
|
|
6
|
+
async function runApprove(packages) {
|
|
7
|
+
if (packages.length === 0) {
|
|
8
|
+
throw new Error("Please provide at least one package to approve.");
|
|
9
|
+
}
|
|
10
|
+
const projectRoot = (0, project_1.findProjectRoot)();
|
|
11
|
+
const store = (0, trustStore_1.readTrustStore)(projectRoot) ?? (0, trustStore_1.createEmptyTrustStore)();
|
|
12
|
+
(0, trustStore_1.addTrustedPackages)(store, dedupe(packages), "manual");
|
|
13
|
+
(0, trustStore_1.writeTrustStore)(projectRoot, store);
|
|
14
|
+
console.log(`Approved package(s): ${dedupe(packages).join(", ")}`);
|
|
15
|
+
}
|
|
16
|
+
function dedupe(items) {
|
|
17
|
+
return [...new Set(items)];
|
|
18
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInit = runInit;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const project_1 = require("../core/project");
|
|
11
|
+
const lockfile_1 = require("../core/lockfile");
|
|
12
|
+
const trustStore_1 = require("../core/trustStore");
|
|
13
|
+
async function runInit(options = {}) {
|
|
14
|
+
const projectRoot = (0, project_1.findProjectRoot)();
|
|
15
|
+
const packageJsonPath = path_1.default.join(projectRoot, "package.json");
|
|
16
|
+
if (!fs_1.default.existsSync(packageJsonPath)) {
|
|
17
|
+
throw new Error("package.json not found. Run this inside an npm project.");
|
|
18
|
+
}
|
|
19
|
+
const trustedFromLockfile = (0, lockfile_1.extractTrustedFromLockfile)(projectRoot);
|
|
20
|
+
const existing = (0, trustStore_1.readTrustStore)(projectRoot);
|
|
21
|
+
const store = existing ?? (0, trustStore_1.createEmptyTrustStore)();
|
|
22
|
+
(0, trustStore_1.addTrustedPackages)(store, [...trustedFromLockfile], "lockfile");
|
|
23
|
+
(0, trustStore_1.writeTrustStore)(projectRoot, store);
|
|
24
|
+
const storePath = (0, trustStore_1.trustStorePath)(projectRoot);
|
|
25
|
+
console.log(`Initialized trust store: ${storePath}`);
|
|
26
|
+
console.log(`Trusted packages imported: ${trustedFromLockfile.size}`);
|
|
27
|
+
if (!options.skipAlias) {
|
|
28
|
+
const shell = detectShell(options.shell);
|
|
29
|
+
const profilePath = ensureAliasInProfile(shell);
|
|
30
|
+
console.log(`Alias configured for ${shell} in: ${profilePath}`);
|
|
31
|
+
console.log("Restart your terminal or reload profile for alias to take effect.");
|
|
32
|
+
}
|
|
33
|
+
if (options.printShellAlias) {
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log("Optional shell alias:");
|
|
36
|
+
console.log(" alias npm='trust-npm'");
|
|
37
|
+
console.log("Windows PowerShell:");
|
|
38
|
+
console.log(" Set-Alias npm trust-npm");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function detectShell(requested) {
|
|
42
|
+
if (requested) {
|
|
43
|
+
return requested;
|
|
44
|
+
}
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
return "powershell";
|
|
47
|
+
}
|
|
48
|
+
const envShell = process.env.SHELL ?? "";
|
|
49
|
+
if (envShell.includes("zsh")) {
|
|
50
|
+
return "zsh";
|
|
51
|
+
}
|
|
52
|
+
return "bash";
|
|
53
|
+
}
|
|
54
|
+
function ensureAliasInProfile(shell) {
|
|
55
|
+
if (shell === "powershell") {
|
|
56
|
+
return ensurePowerShellAlias();
|
|
57
|
+
}
|
|
58
|
+
return ensurePosixAlias(shell);
|
|
59
|
+
}
|
|
60
|
+
function ensurePowerShellAlias() {
|
|
61
|
+
const home = os_1.default.homedir();
|
|
62
|
+
const ps7Path = path_1.default.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
63
|
+
const legacyPath = path_1.default.join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
64
|
+
const profilePath = fs_1.default.existsSync(ps7Path) ? ps7Path : legacyPath;
|
|
65
|
+
ensureFile(profilePath);
|
|
66
|
+
const startMarker = "# trust-npm alias start";
|
|
67
|
+
const endMarker = "# trust-npm alias end";
|
|
68
|
+
const block = `${startMarker}
|
|
69
|
+
Set-Alias npm trust-npm
|
|
70
|
+
${endMarker}
|
|
71
|
+
`;
|
|
72
|
+
appendBlockIfMissing(profilePath, startMarker, block);
|
|
73
|
+
return profilePath;
|
|
74
|
+
}
|
|
75
|
+
function ensurePosixAlias(shell) {
|
|
76
|
+
const home = os_1.default.homedir();
|
|
77
|
+
const filename = shell === "zsh" ? ".zshrc" : ".bashrc";
|
|
78
|
+
const profilePath = path_1.default.join(home, filename);
|
|
79
|
+
ensureFile(profilePath);
|
|
80
|
+
const startMarker = "# trust-npm alias start";
|
|
81
|
+
const endMarker = "# trust-npm alias end";
|
|
82
|
+
const block = `${startMarker}
|
|
83
|
+
alias npm='trust-npm'
|
|
84
|
+
${endMarker}
|
|
85
|
+
`;
|
|
86
|
+
appendBlockIfMissing(profilePath, startMarker, block);
|
|
87
|
+
return profilePath;
|
|
88
|
+
}
|
|
89
|
+
function ensureFile(filePath) {
|
|
90
|
+
const dir = path_1.default.dirname(filePath);
|
|
91
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
92
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
95
|
+
fs_1.default.writeFileSync(filePath, "", "utf8");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function appendBlockIfMissing(filePath, marker, block) {
|
|
99
|
+
const existing = fs_1.default.readFileSync(filePath, "utf8");
|
|
100
|
+
if (existing.includes(marker)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const prefix = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
|
|
104
|
+
fs_1.default.writeFileSync(filePath, `${existing}${prefix}${block}`, "utf8");
|
|
105
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInstall = runInstall;
|
|
7
|
+
const npm_package_arg_1 = __importDefault(require("npm-package-arg"));
|
|
8
|
+
const project_1 = require("../core/project");
|
|
9
|
+
const npm_1 = require("../core/npm");
|
|
10
|
+
const risk_1 = require("../core/risk");
|
|
11
|
+
const trustStore_1 = require("../core/trustStore");
|
|
12
|
+
async function runInstall(rawInstallArgs) {
|
|
13
|
+
const projectRoot = (0, project_1.findProjectRoot)();
|
|
14
|
+
const store = (0, trustStore_1.readTrustStore)(projectRoot);
|
|
15
|
+
if (!store) {
|
|
16
|
+
throw new Error("No .trust-npm.json found. Run `trust-npm init` first.");
|
|
17
|
+
}
|
|
18
|
+
const requestedPackages = extractRequestedPackages(rawInstallArgs);
|
|
19
|
+
if (requestedPackages.length > 0) {
|
|
20
|
+
const unknown = requestedPackages.filter((name) => !store.trustedPackages[name]);
|
|
21
|
+
if (unknown.length > 0) {
|
|
22
|
+
const analyses = await Promise.all(unknown.map((pkg) => (0, risk_1.analyzePackageRisk)(pkg, store.riskThreshold)));
|
|
23
|
+
printBlockedPackages(analyses);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const exitCode = await (0, npm_1.runNpm)(["install", ...rawInstallArgs]);
|
|
29
|
+
process.exitCode = exitCode;
|
|
30
|
+
}
|
|
31
|
+
function extractRequestedPackages(args) {
|
|
32
|
+
const names = new Set();
|
|
33
|
+
for (const token of args) {
|
|
34
|
+
if (!token || token.startsWith("-")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const parsed = (0, npm_package_arg_1.default)(token);
|
|
39
|
+
if (parsed.name) {
|
|
40
|
+
names.add(parsed.name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...names];
|
|
48
|
+
}
|
|
49
|
+
function printBlockedPackages(analyses) {
|
|
50
|
+
for (const analysis of analyses) {
|
|
51
|
+
const header = analysis.isHighRisk ? "BLOCKED (high risk)" : "BLOCKED (untrusted package)";
|
|
52
|
+
console.error(`\n❌ ${header}: ${analysis.packageName}`);
|
|
53
|
+
console.error("Reason:");
|
|
54
|
+
if (analysis.reasons.length > 0) {
|
|
55
|
+
for (const reason of analysis.reasons) {
|
|
56
|
+
console.error(`- ${reason}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.error("- New package is not in trusted baseline");
|
|
61
|
+
}
|
|
62
|
+
console.error(`- Risk score: ${analysis.score}/${analysis.threshold}`);
|
|
63
|
+
console.error("");
|
|
64
|
+
console.error(`Run:\ntrust-npm approve ${analysis.packageName}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runStatus = runStatus;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const project_1 = require("../core/project");
|
|
10
|
+
const trustStore_1 = require("../core/trustStore");
|
|
11
|
+
async function runStatus() {
|
|
12
|
+
const projectRoot = (0, project_1.findProjectRoot)();
|
|
13
|
+
const store = (0, trustStore_1.readTrustStore)(projectRoot);
|
|
14
|
+
if (!store) {
|
|
15
|
+
console.log("No trust store found. Run `trust-npm init` first.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const pkg = readPackageJson(projectRoot);
|
|
19
|
+
const declared = new Set([
|
|
20
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
21
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
22
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
23
|
+
...Object.keys(pkg.optionalDependencies ?? {})
|
|
24
|
+
]);
|
|
25
|
+
const trustedDeclared = [];
|
|
26
|
+
const untrustedDeclared = [];
|
|
27
|
+
for (const name of declared) {
|
|
28
|
+
if (store.trustedPackages[name]) {
|
|
29
|
+
trustedDeclared.push(name);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
untrustedDeclared.push(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
console.log(`Trust store path: ${path_1.default.join(projectRoot, ".trust-npm.json")}`);
|
|
36
|
+
console.log(`Total trusted packages in store: ${Object.keys(store.trustedPackages).length}`);
|
|
37
|
+
console.log(`Declared dependencies: ${declared.size}`);
|
|
38
|
+
console.log(`Trusted declared dependencies: ${trustedDeclared.length}`);
|
|
39
|
+
console.log(`Untrusted declared dependencies: ${untrustedDeclared.length}`);
|
|
40
|
+
if (untrustedDeclared.length > 0) {
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log("Untrusted declared dependencies:");
|
|
43
|
+
for (const dep of untrustedDeclared.sort()) {
|
|
44
|
+
console.log(`- ${dep}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readPackageJson(projectRoot) {
|
|
49
|
+
const filePath = path_1.default.join(projectRoot, "package.json");
|
|
50
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
51
|
+
throw new Error("package.json not found.");
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getLockfilePath = getLockfilePath;
|
|
7
|
+
exports.readLockfile = readLockfile;
|
|
8
|
+
exports.extractTrustedFromLockfile = extractTrustedFromLockfile;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
function getLockfilePath(projectRoot) {
|
|
12
|
+
return path_1.default.join(projectRoot, "package-lock.json");
|
|
13
|
+
}
|
|
14
|
+
function readLockfile(projectRoot) {
|
|
15
|
+
const lockfilePath = getLockfilePath(projectRoot);
|
|
16
|
+
if (!fs_1.default.existsSync(lockfilePath)) {
|
|
17
|
+
throw new Error(`Missing package-lock.json at ${lockfilePath}`);
|
|
18
|
+
}
|
|
19
|
+
const raw = fs_1.default.readFileSync(lockfilePath, "utf8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
function extractTrustedFromLockfile(projectRoot) {
|
|
23
|
+
const lockfile = readLockfile(projectRoot);
|
|
24
|
+
const trusted = new Set();
|
|
25
|
+
if (lockfile.packages) {
|
|
26
|
+
for (const key of Object.keys(lockfile.packages)) {
|
|
27
|
+
if (!key.startsWith("node_modules/")) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const name = key.slice("node_modules/".length);
|
|
31
|
+
if (name) {
|
|
32
|
+
trusted.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (lockfile.dependencies) {
|
|
37
|
+
walkDependencyTree(lockfile.dependencies, trusted);
|
|
38
|
+
}
|
|
39
|
+
return trusted;
|
|
40
|
+
}
|
|
41
|
+
function walkDependencyTree(deps, trusted) {
|
|
42
|
+
for (const [name, dep] of Object.entries(deps)) {
|
|
43
|
+
trusted.add(name);
|
|
44
|
+
if (dep.dependencies) {
|
|
45
|
+
walkDependencyTree(dep.dependencies, trusted);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/core/npm.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runNpm = runNpm;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
async function runNpm(args) {
|
|
6
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = (0, child_process_1.spawn)(npmCommand, args, {
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
shell: false
|
|
11
|
+
});
|
|
12
|
+
child.on("error", (error) => reject(error));
|
|
13
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findProjectRoot = findProjectRoot;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
10
|
+
let current = path_1.default.resolve(startDir);
|
|
11
|
+
const root = path_1.default.parse(current).root;
|
|
12
|
+
while (true) {
|
|
13
|
+
const packageJsonPath = path_1.default.join(current, "package.json");
|
|
14
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
if (current === root) {
|
|
18
|
+
throw new Error("Could not find project root (no package.json found).");
|
|
19
|
+
}
|
|
20
|
+
current = path_1.default.dirname(current);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fetchPackageMetadata = fetchPackageMetadata;
|
|
7
|
+
exports.fetchWeeklyDownloads = fetchWeeklyDownloads;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
async function fetchPackageMetadata(packageName) {
|
|
10
|
+
const encoded = encodeURIComponent(packageName);
|
|
11
|
+
const url = `https://registry.npmjs.org/${encoded}`;
|
|
12
|
+
const response = await axios_1.default.get(url, { timeout: 5000 });
|
|
13
|
+
const data = response.data;
|
|
14
|
+
return {
|
|
15
|
+
name: data.name ?? packageName,
|
|
16
|
+
createdAt: data.time?.created,
|
|
17
|
+
repository: data.repository
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function fetchWeeklyDownloads(packageName) {
|
|
21
|
+
const encoded = encodeURIComponent(packageName);
|
|
22
|
+
const url = `https://api.npmjs.org/downloads/point/last-week/${encoded}`;
|
|
23
|
+
const response = await axios_1.default.get(url, { timeout: 5000 });
|
|
24
|
+
const data = response.data;
|
|
25
|
+
return {
|
|
26
|
+
downloads: typeof data.downloads === "number" ? data.downloads : 0
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzePackageRisk = analyzePackageRisk;
|
|
4
|
+
const registry_1 = require("./registry");
|
|
5
|
+
const NEW_PACKAGE_DAYS_THRESHOLD = 7;
|
|
6
|
+
const LOW_DOWNLOADS_THRESHOLD = 100;
|
|
7
|
+
async function analyzePackageRisk(packageName, threshold) {
|
|
8
|
+
let score = 0;
|
|
9
|
+
const reasons = [];
|
|
10
|
+
const metadata = {};
|
|
11
|
+
const [pkg, downloads] = await Promise.all([
|
|
12
|
+
(0, registry_1.fetchPackageMetadata)(packageName),
|
|
13
|
+
(0, registry_1.fetchWeeklyDownloads)(packageName)
|
|
14
|
+
]);
|
|
15
|
+
const ageDays = getPackageAgeDays(pkg.createdAt);
|
|
16
|
+
metadata.publishedAt = pkg.createdAt;
|
|
17
|
+
metadata.ageDays = ageDays;
|
|
18
|
+
metadata.weeklyDownloads = downloads.downloads;
|
|
19
|
+
if (typeof ageDays === "number" && ageDays < NEW_PACKAGE_DAYS_THRESHOLD) {
|
|
20
|
+
score += 40;
|
|
21
|
+
reasons.push(`Published ${ageDays} day(s) ago`);
|
|
22
|
+
}
|
|
23
|
+
if (downloads.downloads < LOW_DOWNLOADS_THRESHOLD) {
|
|
24
|
+
score += 30;
|
|
25
|
+
reasons.push(`${downloads.downloads} weekly downloads`);
|
|
26
|
+
}
|
|
27
|
+
const hasRepo = hasRepository(pkg.repository);
|
|
28
|
+
metadata.hasRepository = hasRepo;
|
|
29
|
+
if (!hasRepo) {
|
|
30
|
+
score += 20;
|
|
31
|
+
reasons.push("Missing repository field");
|
|
32
|
+
}
|
|
33
|
+
const suspiciousName = hasSuspiciousName(packageName);
|
|
34
|
+
metadata.suspiciousName = suspiciousName;
|
|
35
|
+
if (suspiciousName) {
|
|
36
|
+
score += 10;
|
|
37
|
+
reasons.push("Suspicious naming pattern");
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
packageName,
|
|
41
|
+
score,
|
|
42
|
+
threshold,
|
|
43
|
+
isHighRisk: score > threshold,
|
|
44
|
+
reasons,
|
|
45
|
+
metadata
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function getPackageAgeDays(createdAt) {
|
|
49
|
+
if (!createdAt) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const created = new Date(createdAt);
|
|
53
|
+
if (Number.isNaN(created.valueOf())) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const diffMs = Date.now() - created.getTime();
|
|
57
|
+
return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24)));
|
|
58
|
+
}
|
|
59
|
+
function hasRepository(repository) {
|
|
60
|
+
if (!repository) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (typeof repository === "string") {
|
|
64
|
+
return repository.trim().length > 0;
|
|
65
|
+
}
|
|
66
|
+
if (typeof repository === "object" && repository !== null) {
|
|
67
|
+
const maybeUrl = repository.url;
|
|
68
|
+
return typeof maybeUrl === "string" && maybeUrl.trim().length > 0;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function hasSuspiciousName(name) {
|
|
73
|
+
const normalized = name.toLowerCase();
|
|
74
|
+
const tokens = normalized.split(/[-_.]/g).filter(Boolean);
|
|
75
|
+
if (tokens.length >= 4) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const suspiciousTokens = ["fast", "ultra", "super", "pro", "best", "safe"];
|
|
79
|
+
let suspiciousHits = 0;
|
|
80
|
+
for (const token of tokens) {
|
|
81
|
+
if (suspiciousTokens.includes(token)) {
|
|
82
|
+
suspiciousHits += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return suspiciousHits >= 2;
|
|
86
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TRUST_STORE_FILE = void 0;
|
|
7
|
+
exports.trustStorePath = trustStorePath;
|
|
8
|
+
exports.createEmptyTrustStore = createEmptyTrustStore;
|
|
9
|
+
exports.readTrustStore = readTrustStore;
|
|
10
|
+
exports.writeTrustStore = writeTrustStore;
|
|
11
|
+
exports.addTrustedPackages = addTrustedPackages;
|
|
12
|
+
exports.isTrusted = isTrusted;
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
exports.TRUST_STORE_FILE = ".trust-npm.json";
|
|
16
|
+
const DEFAULT_THRESHOLD = 50;
|
|
17
|
+
function trustStorePath(projectRoot) {
|
|
18
|
+
return path_1.default.join(projectRoot, exports.TRUST_STORE_FILE);
|
|
19
|
+
}
|
|
20
|
+
function createEmptyTrustStore() {
|
|
21
|
+
const now = new Date().toISOString();
|
|
22
|
+
return {
|
|
23
|
+
version: 1,
|
|
24
|
+
createdAt: now,
|
|
25
|
+
updatedAt: now,
|
|
26
|
+
trustedPackages: {},
|
|
27
|
+
riskThreshold: DEFAULT_THRESHOLD
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function readTrustStore(projectRoot) {
|
|
31
|
+
const filePath = trustStorePath(projectRoot);
|
|
32
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const raw = fs_1.default.readFileSync(filePath, "utf8");
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
parsed.riskThreshold = parsed.riskThreshold ?? DEFAULT_THRESHOLD;
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function writeTrustStore(projectRoot, store) {
|
|
41
|
+
const filePath = trustStorePath(projectRoot);
|
|
42
|
+
store.updatedAt = new Date().toISOString();
|
|
43
|
+
fs_1.default.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}\n`, "utf8");
|
|
44
|
+
}
|
|
45
|
+
function addTrustedPackages(store, packageNames, source) {
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
for (const name of packageNames) {
|
|
48
|
+
store.trustedPackages[name] = {
|
|
49
|
+
source,
|
|
50
|
+
approvedAt: now
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return store;
|
|
54
|
+
}
|
|
55
|
+
function isTrusted(store, packageName) {
|
|
56
|
+
return Boolean(store.trustedPackages[packageName]);
|
|
57
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trust-npm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A safe npm wrapper that blocks untrusted dependencies by default.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"trust-npm": "dist/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"start": "node dist/cli.js",
|
|
19
|
+
"dev": "ts-node src/cli.ts"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"npm",
|
|
23
|
+
"security",
|
|
24
|
+
"supply-chain",
|
|
25
|
+
"cli",
|
|
26
|
+
"lockfile"
|
|
27
|
+
],
|
|
28
|
+
"author": "trust-npm contributors",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"axios": "^1.8.4",
|
|
32
|
+
"commander": "^12.1.0",
|
|
33
|
+
"npm-package-arg": "^12.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.14.1",
|
|
37
|
+
"ts-node": "^10.9.2",
|
|
38
|
+
"typescript": "^5.8.2"
|
|
39
|
+
}
|
|
40
|
+
}
|