npm-api-analyzer
Version:
CLI tool to analyze npm packages for network API usage, prototype pollution, and security vulnerabilities
354 lines (299 loc) • 12.7 kB
text/typescript
import { APIDetector } from '../src/apiDetector';
import { APIUsage } from '../src/types';
describe('API Detection', () => {
// Helper function to get all API results
const getAPIResults = (code: string): APIUsage[] => {
return APIDetector.detectAPIUsages(code, 'test.js');
};
// Helper function to filter by category
const getByCategory = (results: APIUsage[], category: string): APIUsage[] => {
return results.filter(r => r.category === category);
};
describe('Network API Detection', () => {
test('should detect fetch API calls', () => {
const code = `
fetch('/api/data');
window.fetch('/api/user');
globalThis.fetch('/api/config');
`;
const results = getByCategory(getAPIResults(code), 'network');
expect(results.length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'fetch').length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'fetch').every(r => r.riskLevel === 'medium')).toBe(true);
});
test('should detect XMLHttpRequest usage', () => {
const code = `
new XMLHttpRequest();
const xhr = XMLHttpRequest();
window.XMLHttpRequest();
`;
const results = getByCategory(getAPIResults(code), 'network');
expect(results.length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'xhr').length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'xhr').every(r => r.riskLevel === 'medium')).toBe(true);
});
test('should detect WebSocket usage', () => {
const code = `
new WebSocket('ws://localhost');
const ws = WebSocket('ws://example.com');
window.WebSocket('wss://secure.com');
`;
const results = getByCategory(getAPIResults(code), 'network');
expect(results.length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'websocket').length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'websocket').every(r => r.riskLevel === 'high')).toBe(true);
});
test('should detect Node.js network modules', () => {
const code = `
require('http');
require('https');
require('node-fetch');
require('axios');
import fetch from 'node-fetch';
import axios from 'axios';
`;
const results = getByCategory(getAPIResults(code), 'network');
expect(results.length).toBeGreaterThan(0);
expect(results.every(r => r.type === 'node')).toBe(true);
expect(results.every(r => r.riskLevel === 'medium')).toBe(true);
});
test('should detect other network APIs', () => {
const code = `
navigator.sendBeacon('/analytics', data);
new EventSource('/stream');
new URL('https://example.com');
new Image();
`;
const results = getByCategory(getAPIResults(code), 'network');
expect(results.length).toBeGreaterThan(0);
expect(results.every(r => r.type === 'other')).toBe(true);
});
});
describe('DOM Manipulation Detection', () => {
test('should detect element creation', () => {
const code = `
document.createElement('div');
document.createTextNode('text');
document.createDocumentFragment();
`;
const results = getByCategory(getAPIResults(code), 'dom');
expect(results).toHaveLength(3);
expect(results.every(r => r.type === 'dom-manipulation')).toBe(true);
});
test('should detect element query methods', () => {
const code = `
document.getElementById('test');
document.querySelector('.class');
document.querySelectorAll('div');
document.getElementsByTagName('p');
document.getElementsByClassName('item');
`;
const results = getByCategory(getAPIResults(code), 'dom');
expect(results).toHaveLength(5);
expect(results.every(r => r.type === 'dom-manipulation')).toBe(true);
expect(results.every(r => r.riskLevel === 'low')).toBe(true);
});
test('should detect high-risk DOM manipulation', () => {
const code = `
element.innerHTML = userInput;
element.outerHTML = dangerousContent;
element.insertAdjacentHTML('beforeend', htmlString);
document.write('<div>content</div>');
document.writeln('text');
`;
const results = getByCategory(getAPIResults(code), 'dom');
expect(results).toHaveLength(5);
expect(results.every(r => r.type === 'dom-manipulation')).toBe(true);
expect(results.every(r => r.riskLevel === 'high')).toBe(true);
});
test('should detect node manipulation methods', () => {
const code = `
parent.appendChild(child);
parent.insertBefore(newNode, referenceNode);
parent.removeChild(child);
parent.replaceChild(newChild, oldChild);
`;
const results = getByCategory(getAPIResults(code), 'dom');
expect(results).toHaveLength(4);
expect(results.every(r => r.type === 'dom-manipulation')).toBe(true);
});
});
describe('Dynamic JavaScript Execution Detection', () => {
test('should detect eval usage', () => {
const code = `
eval('alert("xss")');
window.eval('malicious code');
globalThis.eval('dangerous');
`;
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results.length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'eval').length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'eval').every(r => r.riskLevel === 'high')).toBe(true);
});
test('should detect Function constructor usage', () => {
const code = `
new Function('return malicious');
Function('alert', 'alert("xss")');
window.Function('code');
`;
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results.length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'eval').length).toBeGreaterThanOrEqual(3);
expect(results.filter(r => r.type === 'eval').every(r => r.riskLevel === 'high')).toBe(true);
});
test('should detect dangerous timer usage', () => {
const code = `
setTimeout('alert("xss")', 1000);
setInterval('malicious()', 5000);
`;
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results).toHaveLength(2);
expect(results.every(r => r.type === 'eval')).toBe(true);
expect(results.every(r => r.riskLevel === 'high')).toBe(true);
});
test('should detect dynamic script creation', () => {
const code = `
document.createElement('script');
document.createElement('link');
document.createElement('iframe');
document.head.appendChild(scriptTag);
document.body.appendChild(element);
`;
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results.length).toBeGreaterThan(0);
expect(results.every(r => r.type === 'dynamic-script')).toBe(true);
});
test('should detect dynamic imports', () => {
const code = `
import('./module.js');
require(moduleName);
`;
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results.length).toBeGreaterThanOrEqual(1);
expect(results.filter(r => r.type === 'dynamic-script').length).toBeGreaterThanOrEqual(1);
expect(results.filter(r => r.type === 'dynamic-script').every(r => r.riskLevel === 'medium')).toBe(true);
});
test('should detect Web Workers', () => {
const code = `
new Worker('worker.js');
new SharedWorker('shared-worker.js');
`;
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results).toHaveLength(2);
expect(results.every(r => r.type === 'dynamic-script')).toBe(true);
expect(results.every(r => r.riskLevel === 'medium')).toBe(true);
});
test('should detect Service Worker registration', () => {
const code = 'navigator.serviceWorker.register("sw.js");';
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results).toHaveLength(1);
expect(results[0].type).toBe('dynamic-script');
expect(results[0].riskLevel).toBe('medium');
});
test('should detect Blob URL creation', () => {
const code = `
URL.createObjectURL(blob);
window.URL.createObjectURL(file);
`;
const results = getByCategory(getAPIResults(code), 'dynamic-js');
expect(results.length).toBeGreaterThanOrEqual(2);
expect(results.filter(r => r.type === 'dynamic-script').length).toBeGreaterThanOrEqual(2);
expect(results.filter(r => r.type === 'dynamic-script').every(r => r.riskLevel === 'medium')).toBe(true);
});
});
describe('Context Extraction', () => {
test('should extract context around API calls', () => {
const code = `
// Before line 1
// Before line 2
// Before line 3
const result = fetch('/api/data'); // This is the target line
// After line 1
// After line 2
// After line 3
`;
const results = getAPIResults(code);
expect(results).toHaveLength(1);
const context = results[0].context;
expect(context.beforeLines).toHaveLength(3);
expect(context.afterLines).toHaveLength(3);
expect(context.currentLine).toContain('fetch');
expect(context.fullContext).toContain('Before line');
expect(context.fullContext).toContain('After line');
});
test('should provide accurate location information', () => {
const code = `line1
line2
const api = fetch('/test');
line4`;
const results = getAPIResults(code);
expect(results).toHaveLength(1);
expect(results[0].location.line).toBe(3);
expect(results[0].location.column).toBeGreaterThan(0);
expect(results[0].location.file).toBe('test.js');
});
});
describe('Multiple API Categories', () => {
test('should detect mixed API usage correctly', () => {
const code = `
// Network
fetch('/api/data');
// DOM
document.getElementById('test');
element.innerHTML = content;
// Dynamic JS
eval('code');
// Prototype Pollution
Object.prototype.polluted = true;
`;
const results = getAPIResults(code);
const byCategory = {
network: getByCategory(results, 'network'),
dom: getByCategory(results, 'dom'),
dynamicJs: getByCategory(results, 'dynamic-js'),
prototypePollution: getByCategory(results, 'prototype-pollution')
};
expect(byCategory.network).toHaveLength(1);
expect(byCategory.dom).toHaveLength(2);
expect(byCategory.dynamicJs).toHaveLength(1);
expect(byCategory.prototypePollution).toHaveLength(1);
// Check risk levels
expect(byCategory.network[0].riskLevel).toBe('medium');
expect(byCategory.dom.some(r => r.riskLevel === 'high')).toBe(true);
expect(byCategory.dynamicJs[0].riskLevel).toBe('high');
expect(byCategory.prototypePollution[0].riskLevel).toBe('high');
});
});
describe('Pattern Matching Edge Cases', () => {
test('should handle code in strings and comments', () => {
const code = `
// This is eval('not detected');
const str = "fetch('not detected')";
/* Object.prototype.test = 'not detected'; */
const regex = /innerHTML.*=/;
`;
const results = getAPIResults(code);
// Should detect some patterns in comments/strings due to regex nature
// This is expected behavior for static analysis
expect(results.length).toBeGreaterThan(0);
});
test('should handle minified code', () => {
const code = 'fetch("/api");eval("code");Object.prototype.x=1;';
const results = getAPIResults(code);
expect(results.length).toBeGreaterThan(0);
const categories = ['network', 'dynamic-js', 'prototype-pollution'];
categories.forEach(category => {
expect(getByCategory(results, category).length).toBeGreaterThan(0);
});
});
test('should handle whitespace variations', () => {
const code = `
fetch ( '/api' ) ;
eval ( 'code' ) ;
Object . prototype . test = true ;
`;
const results = getAPIResults(code);
expect(results.length).toBeGreaterThan(0);
});
});
});