
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
impact-scope
Advanced tools
See the blast radius of your code changes. Import graph analysis + risk scoring for PRs.
Import graph analysis and risk scoring for code reviews and PRs.
Import Graph + Risk Scoring + CI Gates
Code review tools show you what changed, but not what those changes will break. impact-scope scans your TypeScript/JavaScript project, builds an import dependency graph, and shows exactly which files are transitively affected when you touch a file. It assigns a risk score from 0 to 100 based on impact depth, affected file count, test coverage gaps, and change size -- so reviewers can prioritize attention on the changes that actually matter.
# Install globally
npm install -g impact-scope
# Analyze the impact of your latest commit
impact-scope analyze
# Analyze changes against a specific base ref
impact-scope analyze --base main
# Check impact of changing a specific file
impact-scope check src/utils/math.ts
# View import graph statistics
impact-scope graph
npm install impact-scope
import {
parseDiff,
buildImportGraph,
analyzeImpact,
buildRiskReport,
formatTerminal,
} from 'impact-scope';
import { execSync } from 'child_process';
// Get diff from git
const diffOutput = execSync('git diff HEAD~1', { encoding: 'utf-8' });
const graph = buildImportGraph('.');
const changedFiles = parseDiff(diffOutput);
const affected = analyzeImpact(changedFiles, graph, '.');
const report = buildRiskReport(changedFiles, affected);
console.log(formatTerminal(report));
impact-scope analyzeAnalyze the impact of code changes from a git diff.
| Option | Default | Description |
|---|---|---|
--base <ref> | HEAD~1 | Base git ref for diff |
--threshold <n> | 50 | Risk score threshold for CI mode |
--format <type> | terminal | Output format: terminal, json, ci |
--root <path> | . | Project root directory |
impact-scope graphShow import graph statistics (file count, edge count, most-imported files).
| Option | Default | Description |
|---|---|---|
--root <path> | . | Project root directory |
impact-scope check <file>Check the blast radius of changing a specific file without needing a diff.
| Option | Default | Description |
|---|---|---|
--format <type> | terminal | Output format: terminal, json |
--root <path> | . | Project root directory |
============================================================
IMPACT SCOPE ANALYSIS
============================================================
Risk Score: 42/100 (MEDIUM)
[#########################---------------]
Changed files: 1
Affected files: 3
Untested: 1
Changed Files:
+2 / -1 src/utils/math.ts
Affected Files (by depth):
depth 1: src/components/calculator.ts (add, multiply) [tested]
depth 1: src/utils/index.ts (*) [tested]
depth 2: src/app.ts [UNTESTED]
Untested Affected Files:
! src/app.ts
============================================================
{
"score": 42,
"level": "medium",
"changedFiles": [
{ "path": "src/utils/math.ts", "additions": 2, "deletions": 1, "hunks": [{ "startLine": 1, "endLine": 6 }] }
],
"affectedFiles": [
{ "filePath": "src/components/calculator.ts", "depth": 1, "affectedSymbols": ["add", "multiply"], "hasTests": true },
{ "filePath": "src/utils/index.ts", "depth": 1, "affectedSymbols": ["*"], "hasTests": true },
{ "filePath": "src/app.ts", "depth": 2, "affectedSymbols": [], "hasTests": false }
],
"untestedAffected": ["src/app.ts"],
"summary": "Risk Score: 42/100 (MEDIUM) | 1 changed file(s), 3 affected file(s), 1 untested, 3 line(s) changed",
"details": [
"--- Changed Files ---",
" src/utils/math.ts (+2/-1)",
"--- Affected Files ---",
" depth=1 src/components/calculator.ts [tested]",
" depth=1 src/utils/index.ts [tested]",
" depth=2 src/app.ts [UNTESTED]",
"--- Untested Affected Files ---",
" ! src/app.ts"
]
}
impact-scope: PASS
score: 42/100 (medium)
threshold: 50
changed: 1 files
affected: 3 files
untested: 1 files
git diff --> parseDiff --> changedFiles
|
project root --> buildImportGraph --> importGraph
|
changedFiles + importGraph --> analyzeImpact --> affectedFiles
|
changedFiles + affectedFiles --> buildRiskReport --> RiskReport
|
formatTerminal / formatJSON / formatCI
The risk score (0-100) is computed from four weighted factors:
| Factor | Weight | Description |
|---|---|---|
| Affected file count | 30% | Number of transitively affected files (caps at 20) |
| Max depth | 20% | Deepest level in the impact chain (caps at 5) |
| Untested ratio | 30% | Fraction of affected files lacking test coverage |
| Change size | 20% | Total lines added + deleted (caps at 500) |
Risk levels: low (0-25), medium (26-50), high (51-75), critical (76-100).
The risk score quantifies how dangerous a set of code changes is to your project. Here is exactly how each factor is computed:
1. Affected File Count (30% weight)
impact-scope walks the reverse import graph using BFS. Starting from each changed file, it finds every file that transitively depends on it. The raw count is divided by a cap of 20 files. If a utility module is imported by 15 files, changing it yields 15/20 = 0.75 for this factor.
2. Max Depth (20% weight)
Depth measures how far the impact ripples through the dependency chain. A change affecting only direct importers has depth 1. If those importers are themselves imported, depth grows. The cap is 5 levels. Depth 3 yields 3/5 = 0.6.
3. Untested Ratio (30% weight)
For each affected file, impact-scope checks whether a corresponding test file exists (e.g., src/foo.ts -> tests/foo.test.ts) or whether any test file in the import graph imports it. The ratio of untested affected files drives this factor. If 4 of 10 affected files lack tests: 4/10 = 0.4.
4. Change Size (20% weight)
Total lines added + deleted across all changed files, capped at 500. A 60-line change yields 60/500 = 0.12.
Final score: (factor1 * 0.3 + factor2 * 0.2 + factor3 * 0.3 + factor4 * 0.2) * 100, rounded and clamped to [0, 100].
You can override the default weights by passing a custom ScoringWeights object to computeRiskScore() or buildRiskReport().
The import graph is a directed graph where each node is a source file and each edge represents an import statement:
math.ts <-- calculator.ts <-- app.ts
| ^
v |
format.ts --> index.ts
How the graph is built:
collectSourceFiles() recursively scans the project root for .ts, .tsx, .js, and .jsx files, skipping node_modules, dist, and .git directories.extractImports() reads each file and uses regex patterns to extract five types of import/export statements:
import { x } from './path' (named imports)import x from './path' (default imports)import * as x from './path' (namespace imports)export { x } from './path' (re-exports)export * from './path' (star re-exports)resolveImportPath() resolves relative import specifiers to actual file paths, trying each extension and index file resolution.How impact is analyzed:
getReverseGraph() inverts the edges so each file maps to its importers.analyzeImpact() runs BFS from each changed file through the reverse graph, recording the depth at which each affected file is reached.Limitations:
@/utils/math) and bare module specifiers (lodash) are not tracked.import() expressions are not detected.paths configuration in tsconfig.json is not read.Add this workflow to .github/workflows/impact-scope.yml to run impact analysis on every PR:
name: Impact Scope Analysis
on:
pull_request:
branches: [main]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Run impact analysis
run: npx impact-scope analyze --base origin/main --format ci --threshold 50
// dangerfile.ts
import { execSync } from 'child_process';
const output = execSync(
'npx impact-scope analyze --base origin/main --format json',
{ encoding: 'utf-8' }
);
const report = JSON.parse(output);
if (report.score > 50) {
warn(`Impact scope risk score: ${report.score}/100 (${report.level})`);
}
if (report.untestedAffected.length > 0) {
const files = report.untestedAffected.map((f: string) => `- \`${f}\``).join('\n');
warn(`Untested affected files:\n${files}`);
}
message(`**Impact Analysis:** ${report.summary}`);
- name: Run impact analysis
id: impact
run: |
OUTPUT=$(npx impact-scope analyze --base origin/main --format json)
echo "report<<EOF" >> $GITHUB_OUTPUT
echo "$OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
const report = JSON.parse(`${{ steps.impact.outputs.report }}`);
const body = [
'## Impact Scope Analysis',
`**Risk Score:** ${report.score}/100 (${report.level.toUpperCase()})`,
`**Changed:** ${report.changedFiles.length} files | **Affected:** ${report.affectedFiles.length} files | **Untested:** ${report.untestedAffected.length}`,
'',
report.untestedAffected.length > 0
? '### Untested Affected Files\n' + report.untestedAffected.map(f => `- \`${f}\``).join('\n')
: '',
].filter(Boolean).join('\n');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
import {
parseDiff,
buildImportGraph,
analyzeImpact,
buildRiskReport,
formatTerminal,
formatJSON,
formatCI,
} from 'impact-scope';
import type { RiskReport } from 'impact-scope';
function analyzeChanges(diffOutput: string, projectRoot: string): RiskReport {
const changed = parseDiff(diffOutput);
const graph = buildImportGraph(projectRoot);
const affected = analyzeImpact(changed, graph, projectRoot);
return buildRiskReport(changed, affected);
}
// Render the report
const report = analyzeChanges(diffOutput, '.');
console.log(formatTerminal(report)); // colored terminal output
console.log(formatJSON(report)); // JSON string
const { output, exitCode } = formatCI(report, 50); // CI with threshold
| Function | Description |
|---|---|
parseDiff(diffOutput) | Parse unified diff string into ChangedFile[] |
buildImportGraph(rootDir) | Scan project and build ImportGraph |
analyzeImpact(changed, graph, root) | Find transitively affected files via BFS |
analyzeFileImpact(filePath, graph, root) | Shorthand for single-file impact check |
computeRiskScore(changed, affected, weights?) | Compute 0-100 risk score |
getRiskLevel(score) | Map score to 'low'/'medium'/'high'/'critical' |
buildRiskReport(changed, affected, weights?) | Build complete RiskReport |
formatTerminal(report) | Render colored terminal output |
formatJSON(report) | Render as formatted JSON string |
formatCI(report, threshold) | Render CI output with { output, exitCode } |
checkTestCoverage(file, root, graph) | Check if a file has test coverage |
isTestFile(filePath) | Check if a path looks like a test file |
findTestFiles(graph) | Find all test files in the graph |
getReverseGraph(graph) | Get reverse dependency map |
Error classes: ImpactScopeError, DiffParseError, GraphBuildError, AnalysisError
Types: ChangedFile, ImportEdge, ImportGraph, ImpactNode, RiskReport, ScoringWeights
No previous commit to diff against. Use --base to specify a valid ref:
impact-scope analyze --base main
The check command only works with files in the import graph (.ts, .tsx, .js, .jsx). Ensure:
--root if running from outside the project directoryThis can happen when:
MIT
FAQs
See the blast radius of your code changes. Import graph analysis + risk scoring for PRs.
We found that impact-scope demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.