@rainy-updates/cli 0.5.2-rc.2 → 0.5.3
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/CHANGELOG.md +107 -0
- package/README.md +105 -22
- package/dist/bin/cli.js +124 -9
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.js +9 -2
- package/dist/commands/audit/runner.js +8 -1
- package/dist/commands/audit/sources/index.js +8 -1
- package/dist/commands/doctor/parser.d.ts +2 -0
- package/dist/commands/doctor/parser.js +92 -0
- package/dist/commands/doctor/runner.d.ts +2 -0
- package/dist/commands/doctor/runner.js +13 -0
- package/dist/commands/resolve/runner.js +3 -0
- package/dist/commands/review/parser.d.ts +2 -0
- package/dist/commands/review/parser.js +174 -0
- package/dist/commands/review/runner.d.ts +2 -0
- package/dist/commands/review/runner.js +30 -0
- package/dist/config/loader.d.ts +3 -0
- package/dist/core/check.js +64 -5
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +6 -0
- package/dist/core/options.d.ts +8 -1
- package/dist/core/options.js +43 -0
- package/dist/core/review-model.d.ts +5 -0
- package/dist/core/review-model.js +372 -0
- package/dist/core/summary.js +11 -2
- package/dist/core/upgrade.d.ts +1 -0
- package/dist/core/upgrade.js +27 -21
- package/dist/core/warm-cache.js +28 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/output/format.d.ts +4 -1
- package/dist/output/format.js +41 -3
- package/dist/output/github.js +6 -1
- package/dist/output/sarif.js +14 -0
- package/dist/registry/npm.d.ts +22 -0
- package/dist/registry/npm.js +33 -4
- package/dist/risk/index.d.ts +3 -0
- package/dist/risk/index.js +24 -0
- package/dist/risk/scorer.d.ts +3 -0
- package/dist/risk/scorer.js +114 -0
- package/dist/risk/signals/fresh-package.d.ts +3 -0
- package/dist/risk/signals/fresh-package.js +22 -0
- package/dist/risk/signals/install-scripts.d.ts +3 -0
- package/dist/risk/signals/install-scripts.js +10 -0
- package/dist/risk/signals/maintainer-churn.d.ts +3 -0
- package/dist/risk/signals/maintainer-churn.js +11 -0
- package/dist/risk/signals/metadata.d.ts +3 -0
- package/dist/risk/signals/metadata.js +18 -0
- package/dist/risk/signals/mutable-source.d.ts +3 -0
- package/dist/risk/signals/mutable-source.js +24 -0
- package/dist/risk/signals/typosquat.d.ts +3 -0
- package/dist/risk/signals/typosquat.js +70 -0
- package/dist/risk/types.d.ts +15 -0
- package/dist/risk/types.js +1 -0
- package/dist/types/index.d.ts +85 -0
- package/dist/ui/tui.d.ts +0 -4
- package/dist/ui/tui.js +103 -21
- package/package.json +10 -2
package/dist/output/sarif.js
CHANGED
|
@@ -30,6 +30,15 @@ export function createSarifReport(result) {
|
|
|
30
30
|
kind: update.kind,
|
|
31
31
|
diffType: update.diffType,
|
|
32
32
|
resolvedVersion: update.toVersionResolved,
|
|
33
|
+
impactRank: update.impactScore?.rank,
|
|
34
|
+
impactScore: update.impactScore?.score,
|
|
35
|
+
riskLevel: update.riskLevel,
|
|
36
|
+
riskScore: update.riskScore,
|
|
37
|
+
riskCategories: update.riskCategories ?? [],
|
|
38
|
+
recommendedAction: update.recommendedAction,
|
|
39
|
+
advisoryCount: update.advisoryCount ?? 0,
|
|
40
|
+
peerConflictSeverity: update.peerConflictSeverity ?? "none",
|
|
41
|
+
licenseStatus: update.licenseStatus ?? "allowed",
|
|
33
42
|
},
|
|
34
43
|
}));
|
|
35
44
|
const errorResults = [...result.errors].sort((a, b) => a.localeCompare(b)).map((error) => ({
|
|
@@ -75,6 +84,11 @@ export function createSarifReport(result) {
|
|
|
75
84
|
prLimitHit: result.summary.prLimitHit,
|
|
76
85
|
fixPrBranchesCreated: result.summary.fixPrBranchesCreated,
|
|
77
86
|
durationMs: result.summary.durationMs,
|
|
87
|
+
verdict: result.summary.verdict,
|
|
88
|
+
riskPackages: result.summary.riskPackages ?? 0,
|
|
89
|
+
securityPackages: result.summary.securityPackages ?? 0,
|
|
90
|
+
peerConflictPackages: result.summary.peerConflictPackages ?? 0,
|
|
91
|
+
licenseViolationPackages: result.summary.licenseViolationPackages ?? 0,
|
|
78
92
|
},
|
|
79
93
|
},
|
|
80
94
|
],
|
package/dist/registry/npm.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
interface RegistryConfig {
|
|
2
|
+
defaultRegistry: string;
|
|
3
|
+
scopedRegistries: Map<string, string>;
|
|
4
|
+
authByRegistry: Map<string, RegistryAuth>;
|
|
5
|
+
}
|
|
6
|
+
interface RegistryAuth {
|
|
7
|
+
token?: string;
|
|
8
|
+
basicAuth?: string;
|
|
9
|
+
alwaysAuth: boolean;
|
|
10
|
+
}
|
|
1
11
|
export interface ResolveManyOptions {
|
|
2
12
|
concurrency: number;
|
|
3
13
|
timeoutMs?: number;
|
|
@@ -12,6 +22,10 @@ export interface ResolveManyResult {
|
|
|
12
22
|
latestVersion: string | null;
|
|
13
23
|
versions: string[];
|
|
14
24
|
publishedAtByVersion: Record<string, number>;
|
|
25
|
+
homepage?: string;
|
|
26
|
+
repository?: string;
|
|
27
|
+
installScriptByVersion: Record<string, boolean>;
|
|
28
|
+
maintainerCount: number | null;
|
|
15
29
|
}>;
|
|
16
30
|
errors: Map<string, string>;
|
|
17
31
|
}
|
|
@@ -24,6 +38,10 @@ export declare class NpmRegistryClient {
|
|
|
24
38
|
latestVersion: string | null;
|
|
25
39
|
versions: string[];
|
|
26
40
|
publishedAtByVersion: Record<string, number>;
|
|
41
|
+
homepage?: string;
|
|
42
|
+
repository?: string;
|
|
43
|
+
installScriptByVersion: Record<string, boolean>;
|
|
44
|
+
maintainerCount: number | null;
|
|
27
45
|
}>;
|
|
28
46
|
resolveLatestVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
|
|
29
47
|
resolveManyPackageMetadata(packageNames: string[], options: ResolveManyOptions): Promise<ResolveManyResult>;
|
|
@@ -32,3 +50,7 @@ export declare class NpmRegistryClient {
|
|
|
32
50
|
errors: Map<string, string>;
|
|
33
51
|
}>;
|
|
34
52
|
}
|
|
53
|
+
export declare function loadRegistryConfig(cwd: string): Promise<RegistryConfig>;
|
|
54
|
+
export declare function resolveRegistryForPackage(packageName: string, config: RegistryConfig): string;
|
|
55
|
+
export declare function resolveAuthHeader(registry: string, config: RegistryConfig): string | undefined;
|
|
56
|
+
export {};
|
package/dist/registry/npm.js
CHANGED
|
@@ -22,7 +22,13 @@ export class NpmRegistryClient {
|
|
|
22
22
|
try {
|
|
23
23
|
const response = await requester(packageName, timeoutMs);
|
|
24
24
|
if (response.status === 404) {
|
|
25
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
latestVersion: null,
|
|
27
|
+
versions: [],
|
|
28
|
+
publishedAtByVersion: {},
|
|
29
|
+
installScriptByVersion: {},
|
|
30
|
+
maintainerCount: null,
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
33
|
if (response.status === 429 || response.status >= 500) {
|
|
28
34
|
throw new RetryableRegistryError(`Registry temporary error: ${response.status}`, response.retryAfterMs ?? computeBackoffMs(attempt));
|
|
@@ -35,6 +41,12 @@ export class NpmRegistryClient {
|
|
|
35
41
|
latestVersion: response.data?.["dist-tags"]?.latest ?? null,
|
|
36
42
|
versions,
|
|
37
43
|
publishedAtByVersion: extractPublishTimes(response.data?.time),
|
|
44
|
+
homepage: response.data?.homepage,
|
|
45
|
+
repository: normalizeRepository(response.data?.repository),
|
|
46
|
+
installScriptByVersion: detectInstallScriptsByVersion(response.data?.versions),
|
|
47
|
+
maintainerCount: Array.isArray(response.data?.maintainers)
|
|
48
|
+
? response.data?.maintainers.length
|
|
49
|
+
: null,
|
|
38
50
|
};
|
|
39
51
|
}
|
|
40
52
|
catch (error) {
|
|
@@ -94,6 +106,23 @@ export class NpmRegistryClient {
|
|
|
94
106
|
function sleep(ms) {
|
|
95
107
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
96
108
|
}
|
|
109
|
+
function normalizeRepository(value) {
|
|
110
|
+
if (!value)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (typeof value === "string")
|
|
113
|
+
return value;
|
|
114
|
+
return value.url;
|
|
115
|
+
}
|
|
116
|
+
function detectInstallScriptsByVersion(versions) {
|
|
117
|
+
if (!versions)
|
|
118
|
+
return {};
|
|
119
|
+
const results = {};
|
|
120
|
+
for (const [version, metadata] of Object.entries(versions)) {
|
|
121
|
+
const scripts = metadata?.scripts;
|
|
122
|
+
results[version] = Boolean(scripts?.preinstall || scripts?.install || scripts?.postinstall);
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
97
126
|
function computeBackoffMs(attempt) {
|
|
98
127
|
const baseMs = Math.max(120, attempt * 180);
|
|
99
128
|
const jitterMs = Math.floor(Math.random() * 120);
|
|
@@ -195,7 +224,7 @@ async function tryCreateUndiciRequester(registryConfig) {
|
|
|
195
224
|
return null;
|
|
196
225
|
}
|
|
197
226
|
}
|
|
198
|
-
async function loadRegistryConfig(cwd) {
|
|
227
|
+
export async function loadRegistryConfig(cwd) {
|
|
199
228
|
const homeNpmrc = path.join(os.homedir(), ".npmrc");
|
|
200
229
|
const projectNpmrc = path.join(cwd, ".npmrc");
|
|
201
230
|
const merged = new Map();
|
|
@@ -274,7 +303,7 @@ function normalizeRegistryUrl(value) {
|
|
|
274
303
|
const normalized = value.endsWith("/") ? value : `${value}/`;
|
|
275
304
|
return normalized;
|
|
276
305
|
}
|
|
277
|
-
function resolveRegistryForPackage(packageName, config) {
|
|
306
|
+
export function resolveRegistryForPackage(packageName, config) {
|
|
278
307
|
const scope = extractScope(packageName);
|
|
279
308
|
if (scope) {
|
|
280
309
|
const scoped = config.scopedRegistries.get(scope);
|
|
@@ -295,7 +324,7 @@ function buildRegistryUrl(registry, packageName) {
|
|
|
295
324
|
const base = normalizeRegistryUrl(registry);
|
|
296
325
|
return new URL(encodeURIComponent(packageName), base).toString();
|
|
297
326
|
}
|
|
298
|
-
function resolveAuthHeader(registry, config) {
|
|
327
|
+
export function resolveAuthHeader(registry, config) {
|
|
299
328
|
const registryUrl = normalizeRegistryUrl(registry);
|
|
300
329
|
const auth = findRegistryAuth(registryUrl, config.authByRegistry);
|
|
301
330
|
if (!auth)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { assessRisk } from "./scorer.js";
|
|
2
|
+
export function applyRiskAssessments(items, context) {
|
|
3
|
+
return items.map((item) => {
|
|
4
|
+
const assessment = assessRisk({
|
|
5
|
+
update: item.update,
|
|
6
|
+
advisories: item.advisories,
|
|
7
|
+
health: item.health,
|
|
8
|
+
peerConflicts: item.peerConflicts,
|
|
9
|
+
licenseViolation: item.update.licenseStatus === "denied",
|
|
10
|
+
unusedIssues: item.unusedIssues,
|
|
11
|
+
}, context);
|
|
12
|
+
return {
|
|
13
|
+
...item,
|
|
14
|
+
update: {
|
|
15
|
+
...item.update,
|
|
16
|
+
riskLevel: assessment.level,
|
|
17
|
+
riskScore: assessment.score,
|
|
18
|
+
riskReasons: assessment.reasons,
|
|
19
|
+
riskCategories: assessment.categories,
|
|
20
|
+
recommendedAction: assessment.recommendedAction,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { detectInstallScriptsRisk } from "./signals/install-scripts.js";
|
|
2
|
+
import { detectTyposquatRisk } from "./signals/typosquat.js";
|
|
3
|
+
import { detectFreshPackageRisk } from "./signals/fresh-package.js";
|
|
4
|
+
import { detectSuspiciousMetadataRisk } from "./signals/metadata.js";
|
|
5
|
+
import { detectMutableSourceRisk } from "./signals/mutable-source.js";
|
|
6
|
+
import { detectMaintainerChurnRisk } from "./signals/maintainer-churn.js";
|
|
7
|
+
export function assessRisk(input, context) {
|
|
8
|
+
// Base factors model direct supply-chain behavior; modifiers adjust operational severity.
|
|
9
|
+
const baseFactors = [];
|
|
10
|
+
const modifierFactors = [];
|
|
11
|
+
if (input.advisories.length > 0) {
|
|
12
|
+
baseFactors.push({
|
|
13
|
+
code: "known-vulnerability",
|
|
14
|
+
weight: 35,
|
|
15
|
+
category: "known-vulnerability",
|
|
16
|
+
message: `${input.advisories.length} known vulnerability finding(s) affect this package.`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const installScripts = detectInstallScriptsRisk(input);
|
|
20
|
+
if (installScripts)
|
|
21
|
+
baseFactors.push(installScripts);
|
|
22
|
+
const typosquat = detectTyposquatRisk(input, context);
|
|
23
|
+
if (typosquat)
|
|
24
|
+
baseFactors.push(typosquat);
|
|
25
|
+
const freshPackage = detectFreshPackageRisk(input);
|
|
26
|
+
if (freshPackage)
|
|
27
|
+
baseFactors.push(freshPackage);
|
|
28
|
+
const metadata = detectSuspiciousMetadataRisk(input);
|
|
29
|
+
if (metadata)
|
|
30
|
+
baseFactors.push(metadata);
|
|
31
|
+
const mutableSource = detectMutableSourceRisk(input);
|
|
32
|
+
if (mutableSource)
|
|
33
|
+
baseFactors.push(mutableSource);
|
|
34
|
+
const maintainerChurn = detectMaintainerChurnRisk(input);
|
|
35
|
+
if (maintainerChurn)
|
|
36
|
+
baseFactors.push(maintainerChurn);
|
|
37
|
+
if (input.peerConflicts.some((conflict) => conflict.severity === "error")) {
|
|
38
|
+
modifierFactors.push({
|
|
39
|
+
code: "peer-conflict",
|
|
40
|
+
weight: 20,
|
|
41
|
+
category: "operational-health",
|
|
42
|
+
message: "Peer dependency conflicts block safe application.",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (input.licenseViolation) {
|
|
46
|
+
modifierFactors.push({
|
|
47
|
+
code: "license-violation",
|
|
48
|
+
weight: 20,
|
|
49
|
+
category: "operational-health",
|
|
50
|
+
message: "License policy would block or require review for this update.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (input.health?.flags.includes("deprecated")) {
|
|
54
|
+
modifierFactors.push({
|
|
55
|
+
code: "deprecated-package",
|
|
56
|
+
weight: 10,
|
|
57
|
+
category: "operational-health",
|
|
58
|
+
message: "Package is deprecated.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else if (input.health?.flags.includes("stale") ||
|
|
62
|
+
input.health?.flags.includes("unmaintained")) {
|
|
63
|
+
modifierFactors.push({
|
|
64
|
+
code: "stale-package",
|
|
65
|
+
weight: 5,
|
|
66
|
+
category: "operational-health",
|
|
67
|
+
message: "Package has stale operational health signals.",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (input.update.diffType === "major") {
|
|
71
|
+
modifierFactors.push({
|
|
72
|
+
code: "major-version",
|
|
73
|
+
weight: 10,
|
|
74
|
+
category: "operational-health",
|
|
75
|
+
message: "Update crosses a major version boundary.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const baseScore = baseFactors.reduce((sum, factor) => sum + factor.weight, 0);
|
|
79
|
+
const modifierScore = modifierFactors.reduce((sum, factor) => sum + factor.weight, 0);
|
|
80
|
+
const factors = [...baseFactors, ...modifierFactors];
|
|
81
|
+
const score = Math.min(100, baseScore + modifierScore);
|
|
82
|
+
const level = scoreToLevel(score);
|
|
83
|
+
const categories = Array.from(new Set(factors.map((factor) => factor.category)));
|
|
84
|
+
const reasons = factors.map((factor) => factor.message);
|
|
85
|
+
return {
|
|
86
|
+
score,
|
|
87
|
+
level,
|
|
88
|
+
reasons,
|
|
89
|
+
categories,
|
|
90
|
+
recommendedAction: recommendAction(level, input),
|
|
91
|
+
factors,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function scoreToLevel(score) {
|
|
95
|
+
if (score >= 70)
|
|
96
|
+
return "critical";
|
|
97
|
+
if (score >= 45)
|
|
98
|
+
return "high";
|
|
99
|
+
if (score >= 20)
|
|
100
|
+
return "medium";
|
|
101
|
+
return "low";
|
|
102
|
+
}
|
|
103
|
+
function recommendAction(level, input) {
|
|
104
|
+
if (input.peerConflicts.some((conflict) => conflict.severity === "error")) {
|
|
105
|
+
return "Run `rup resolve --after-update` before applying this update.";
|
|
106
|
+
}
|
|
107
|
+
if (input.advisories.length > 0) {
|
|
108
|
+
return "Review in `rup review` and consider `rup audit --fix` for the secure minimum patch.";
|
|
109
|
+
}
|
|
110
|
+
if (level === "critical" || level === "high") {
|
|
111
|
+
return "Keep this update in review until the risk reasons are cleared.";
|
|
112
|
+
}
|
|
113
|
+
return "Safe to keep in the review queue and apply after normal verification.";
|
|
114
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function detectFreshPackageRisk(input) {
|
|
2
|
+
const age = input.update.publishAgeDays;
|
|
3
|
+
if (typeof age !== "number")
|
|
4
|
+
return null;
|
|
5
|
+
if (age <= 7) {
|
|
6
|
+
return {
|
|
7
|
+
code: "fresh-package-7d",
|
|
8
|
+
weight: 20,
|
|
9
|
+
category: "behavioral-risk",
|
|
10
|
+
message: `Resolved version was published ${age} day(s) ago.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (age <= 30) {
|
|
14
|
+
return {
|
|
15
|
+
code: "fresh-package-30d",
|
|
16
|
+
weight: 10,
|
|
17
|
+
category: "behavioral-risk",
|
|
18
|
+
message: `Resolved version was published ${age} day(s) ago.`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function detectInstallScriptsRisk(input) {
|
|
2
|
+
if (!input.update.hasInstallScript)
|
|
3
|
+
return null;
|
|
4
|
+
return {
|
|
5
|
+
code: "install-scripts",
|
|
6
|
+
weight: 20,
|
|
7
|
+
category: "behavioral-risk",
|
|
8
|
+
message: "Resolved package includes install lifecycle scripts.",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function detectMaintainerChurnRisk(input) {
|
|
2
|
+
if (input.update.maintainerChurn !== "elevated-change") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
return {
|
|
6
|
+
code: "maintainer-churn",
|
|
7
|
+
weight: 15,
|
|
8
|
+
category: "behavioral-risk",
|
|
9
|
+
message: "Maintainer profile looks unstable for a recent release based on available registry metadata.",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function detectSuspiciousMetadataRisk(input) {
|
|
2
|
+
const { homepage, repository } = input.update;
|
|
3
|
+
const homepageMissing = !homepage;
|
|
4
|
+
const repositoryMissing = !repository;
|
|
5
|
+
const repositoryMalformed = typeof repository === "string" &&
|
|
6
|
+
!repository.startsWith("http://") &&
|
|
7
|
+
!repository.startsWith("https://") &&
|
|
8
|
+
!repository.startsWith("git+");
|
|
9
|
+
if ((homepageMissing && repositoryMissing) || repositoryMalformed) {
|
|
10
|
+
return {
|
|
11
|
+
code: "suspicious-metadata",
|
|
12
|
+
weight: 10,
|
|
13
|
+
category: "behavioral-risk",
|
|
14
|
+
message: "Package metadata is incomplete or uses a non-canonical repository reference.",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const MUTABLE_PATTERNS = [
|
|
2
|
+
"git+",
|
|
3
|
+
"github:",
|
|
4
|
+
"gitlab:",
|
|
5
|
+
"http://",
|
|
6
|
+
"https://",
|
|
7
|
+
"git://",
|
|
8
|
+
];
|
|
9
|
+
export function detectMutableSourceRisk(input) {
|
|
10
|
+
const raw = input.update.fromRange;
|
|
11
|
+
if (!MUTABLE_PATTERNS.some((pattern) => raw.startsWith(pattern))) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const immutableCommitPinned = /#[a-f0-9]{7,40}$/i.test(raw);
|
|
15
|
+
if (immutableCommitPinned) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
code: "mutable-source",
|
|
20
|
+
weight: 25,
|
|
21
|
+
category: "behavioral-risk",
|
|
22
|
+
message: "Dependency uses a mutable git/http source without an immutable commit pin.",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const HIGH_VALUE_PACKAGES = [
|
|
2
|
+
"react",
|
|
3
|
+
"react-dom",
|
|
4
|
+
"next",
|
|
5
|
+
"typescript",
|
|
6
|
+
"lodash",
|
|
7
|
+
"axios",
|
|
8
|
+
"zod",
|
|
9
|
+
"vite",
|
|
10
|
+
"eslint",
|
|
11
|
+
"express",
|
|
12
|
+
];
|
|
13
|
+
export function detectTyposquatRisk(input, context) {
|
|
14
|
+
const target = normalizeName(input.update.name);
|
|
15
|
+
if (target.length < 4)
|
|
16
|
+
return null;
|
|
17
|
+
const candidates = new Set([
|
|
18
|
+
...HIGH_VALUE_PACKAGES,
|
|
19
|
+
...Array.from(context.knownPackageNames),
|
|
20
|
+
]);
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
const normalizedCandidate = normalizeName(candidate);
|
|
23
|
+
if (!normalizedCandidate || normalizedCandidate === target)
|
|
24
|
+
continue;
|
|
25
|
+
if (Math.abs(normalizedCandidate.length - target.length) > 1)
|
|
26
|
+
continue;
|
|
27
|
+
if (isTransposition(normalizedCandidate, target) || levenshtein(normalizedCandidate, target) === 1) {
|
|
28
|
+
return {
|
|
29
|
+
code: "typosquat-heuristic",
|
|
30
|
+
weight: 25,
|
|
31
|
+
category: "behavioral-risk",
|
|
32
|
+
message: `Package name is highly similar to "${candidate}", which may indicate typosquatting.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function normalizeName(value) {
|
|
39
|
+
const trimmed = value.startsWith("@") ? value.split("/")[1] ?? value : value;
|
|
40
|
+
return trimmed.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
function isTransposition(left, right) {
|
|
43
|
+
if (left.length !== right.length || left === right)
|
|
44
|
+
return false;
|
|
45
|
+
const mismatches = [];
|
|
46
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
47
|
+
if (left[index] !== right[index])
|
|
48
|
+
mismatches.push(index);
|
|
49
|
+
if (mismatches.length > 2)
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (mismatches.length !== 2)
|
|
53
|
+
return false;
|
|
54
|
+
const [first, second] = mismatches;
|
|
55
|
+
return left[first] === right[second] && left[second] === right[first];
|
|
56
|
+
}
|
|
57
|
+
function levenshtein(left, right) {
|
|
58
|
+
const matrix = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
|
|
59
|
+
for (let i = 0; i <= left.length; i += 1)
|
|
60
|
+
matrix[i][0] = i;
|
|
61
|
+
for (let j = 0; j <= right.length; j += 1)
|
|
62
|
+
matrix[0][j] = j;
|
|
63
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
64
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
65
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
66
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return matrix[left.length][right.length];
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HealthResult, PackageUpdate, PeerConflict, RiskAssessment, UnusedDependency, CveAdvisory } from "../types/index.js";
|
|
2
|
+
export interface RiskInput {
|
|
3
|
+
update: PackageUpdate;
|
|
4
|
+
advisories: CveAdvisory[];
|
|
5
|
+
health?: HealthResult["metrics"][number];
|
|
6
|
+
peerConflicts: PeerConflict[];
|
|
7
|
+
licenseViolation: boolean;
|
|
8
|
+
unusedIssues: UnusedDependency[];
|
|
9
|
+
}
|
|
10
|
+
export interface RiskContext {
|
|
11
|
+
knownPackageNames: ReadonlySet<string>;
|
|
12
|
+
}
|
|
13
|
+
export interface RiskSignalResult {
|
|
14
|
+
assessment: RiskAssessment;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export type TargetLevel = "patch" | "minor" | "major" | "latest";
|
|
|
3
3
|
export type GroupBy = "none" | "name" | "scope" | "kind" | "risk";
|
|
4
4
|
export type CiProfile = "minimal" | "strict" | "enterprise";
|
|
5
5
|
export type LockfileMode = "preserve" | "update" | "error";
|
|
6
|
+
export type Verdict = "safe" | "review" | "blocked" | "actionable";
|
|
7
|
+
export type RiskLevel = "critical" | "high" | "medium" | "low";
|
|
8
|
+
export type RiskCategory = "known-vulnerability" | "behavioral-risk" | "operational-health";
|
|
9
|
+
export type MaintainerChurnStatus = "unknown" | "stable" | "elevated-change";
|
|
6
10
|
export type OutputFormat = "table" | "json" | "minimal" | "github" | "metrics";
|
|
7
11
|
export type FailOnLevel = "none" | "patch" | "minor" | "major" | "any";
|
|
8
12
|
export type LogLevel = "error" | "warn" | "info" | "debug";
|
|
@@ -44,6 +48,9 @@ export interface RunOptions {
|
|
|
44
48
|
onlyChanged: boolean;
|
|
45
49
|
ciProfile: CiProfile;
|
|
46
50
|
lockfileMode: LockfileMode;
|
|
51
|
+
interactive: boolean;
|
|
52
|
+
showImpact: boolean;
|
|
53
|
+
showHomepage: boolean;
|
|
47
54
|
}
|
|
48
55
|
export interface CheckOptions extends RunOptions {
|
|
49
56
|
}
|
|
@@ -86,6 +93,21 @@ export interface PackageUpdate {
|
|
|
86
93
|
reason?: string;
|
|
87
94
|
impactScore?: ImpactScore;
|
|
88
95
|
homepage?: string;
|
|
96
|
+
repository?: string;
|
|
97
|
+
publishedAt?: string;
|
|
98
|
+
publishAgeDays?: number | null;
|
|
99
|
+
hasInstallScript?: boolean;
|
|
100
|
+
maintainerCount?: number | null;
|
|
101
|
+
maintainerChurn?: MaintainerChurnStatus;
|
|
102
|
+
riskLevel?: RiskLevel;
|
|
103
|
+
riskScore?: number;
|
|
104
|
+
riskReasons?: string[];
|
|
105
|
+
riskCategories?: RiskCategory[];
|
|
106
|
+
recommendedAction?: string;
|
|
107
|
+
advisoryCount?: number;
|
|
108
|
+
peerConflictSeverity?: "none" | PeerConflictSeverity;
|
|
109
|
+
licenseStatus?: "allowed" | "review" | "denied";
|
|
110
|
+
healthStatus?: "healthy" | HealthFlag;
|
|
89
111
|
}
|
|
90
112
|
export interface Summary {
|
|
91
113
|
contractVersion: "2";
|
|
@@ -126,6 +148,13 @@ export interface Summary {
|
|
|
126
148
|
prLimitHit: boolean;
|
|
127
149
|
streamedEvents: number;
|
|
128
150
|
policyOverridesApplied: number;
|
|
151
|
+
verdict?: Verdict;
|
|
152
|
+
interactiveSession?: boolean;
|
|
153
|
+
riskPackages?: number;
|
|
154
|
+
securityPackages?: number;
|
|
155
|
+
peerConflictPackages?: number;
|
|
156
|
+
licenseViolationPackages?: number;
|
|
157
|
+
privateRegistryPackages?: number;
|
|
129
158
|
}
|
|
130
159
|
export interface CheckResult {
|
|
131
160
|
projectPath: string;
|
|
@@ -303,6 +332,62 @@ export interface ResolveResult {
|
|
|
303
332
|
errors: string[];
|
|
304
333
|
warnings: string[];
|
|
305
334
|
}
|
|
335
|
+
export interface RiskSignal {
|
|
336
|
+
packageName: string;
|
|
337
|
+
code: string;
|
|
338
|
+
weight: number;
|
|
339
|
+
category: RiskCategory;
|
|
340
|
+
level: RiskLevel;
|
|
341
|
+
reasons: string[];
|
|
342
|
+
}
|
|
343
|
+
export interface RiskFactor {
|
|
344
|
+
code: string;
|
|
345
|
+
weight: number;
|
|
346
|
+
category: RiskCategory;
|
|
347
|
+
message: string;
|
|
348
|
+
}
|
|
349
|
+
export interface RiskAssessment {
|
|
350
|
+
score: number;
|
|
351
|
+
level: RiskLevel;
|
|
352
|
+
reasons: string[];
|
|
353
|
+
categories: RiskCategory[];
|
|
354
|
+
recommendedAction: string;
|
|
355
|
+
factors: RiskFactor[];
|
|
356
|
+
}
|
|
357
|
+
export interface ReviewItem {
|
|
358
|
+
update: PackageUpdate;
|
|
359
|
+
advisories: CveAdvisory[];
|
|
360
|
+
health?: PackageHealthMetric;
|
|
361
|
+
peerConflicts: PeerConflict[];
|
|
362
|
+
license?: PackageLicense;
|
|
363
|
+
unusedIssues: UnusedDependency[];
|
|
364
|
+
selected: boolean;
|
|
365
|
+
}
|
|
366
|
+
export interface ReviewResult {
|
|
367
|
+
projectPath: string;
|
|
368
|
+
target: TargetLevel;
|
|
369
|
+
summary: Summary;
|
|
370
|
+
items: ReviewItem[];
|
|
371
|
+
updates: PackageUpdate[];
|
|
372
|
+
errors: string[];
|
|
373
|
+
warnings: string[];
|
|
374
|
+
}
|
|
375
|
+
export interface ReviewOptions extends CheckOptions {
|
|
376
|
+
securityOnly: boolean;
|
|
377
|
+
risk?: RiskLevel;
|
|
378
|
+
diff?: TargetLevel;
|
|
379
|
+
applySelected: boolean;
|
|
380
|
+
}
|
|
381
|
+
export interface DoctorOptions extends CheckOptions {
|
|
382
|
+
verdictOnly: boolean;
|
|
383
|
+
}
|
|
384
|
+
export interface DoctorResult {
|
|
385
|
+
verdict: Verdict;
|
|
386
|
+
summary: Summary;
|
|
387
|
+
review: ReviewResult;
|
|
388
|
+
primaryFindings: string[];
|
|
389
|
+
recommendedCommand: string;
|
|
390
|
+
}
|
|
306
391
|
export type UnusedKind = "declared-not-imported" | "imported-not-declared";
|
|
307
392
|
export interface UnusedDependency {
|
|
308
393
|
name: string;
|
package/dist/ui/tui.d.ts
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
1
|
import type { PackageUpdate } from "../types/index.js";
|
|
2
|
-
export declare function VersionDiff({ from, to }: {
|
|
3
|
-
from: string;
|
|
4
|
-
to: string;
|
|
5
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
6
2
|
export declare function runTui(updates: PackageUpdate[]): Promise<PackageUpdate[]>;
|