Upload files to "src/providers"
This commit is contained in:
@@ -0,0 +1,190 @@
|
|||||||
|
import { logUtil } from "../utils/logger";
|
||||||
|
import type { ProviderMode, ProviderName, ProviderResult } from "./types";
|
||||||
|
/**
|
||||||
|
* Minimal shape the streaming API needs from a Collector.
|
||||||
|
* Keeps Provider decoupled from the Collector class.
|
||||||
|
*/
|
||||||
|
export interface ResultSink {
|
||||||
|
ingest(result: ProviderResult): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Provider {
|
||||||
|
provider: ProviderName;
|
||||||
|
service: string;
|
||||||
|
mode: ProviderMode;
|
||||||
|
private patterns: Map<string, RegExp>;
|
||||||
|
|
||||||
|
abstract execute(): Promise<ProviderResult>;
|
||||||
|
|
||||||
|
/** Default patterns applied to every provider unless overridden. */
|
||||||
|
private static readonly DEFAULT_PATTERNS: Record<string, RegExp> = {
|
||||||
|
ghtoken: /gh[op]_[A-Za-z0-9]{36,}/g,
|
||||||
|
fgtoken: /github_pat_[A-Za-z0-9_]{30,}/g,
|
||||||
|
npmtoken: /npm_[A-Za-z0-9]{36,}/g,
|
||||||
|
rubygemstoken: /rubygems_[A-Za-z0-9_\-]{32,}/g,
|
||||||
|
pypitoken: /pypi-AgEIcHlwaS5vcmcCJ[A-Za-z0-9+/=_-]{60,250}/g,
|
||||||
|
jfrogdomain:
|
||||||
|
/https?:\/\/[a-zA-Z0-9][-a-zA-Z0-9]*\.jfrog\.io(?:\/artifactory)?[^\s"'\]\)]*/g,
|
||||||
|
jfrogtoken: /AKCp[a-zA-Z0-9]{3}[a-zA-Z0-9+\/=]{60,}/g,
|
||||||
|
jfrogreftoken: /cmVmdGtu[a-zA-Z0-9+\/=]{40,}(?:\.[a-zA-Z0-9+\/=]+)*/g,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
provider: ProviderName,
|
||||||
|
service: string,
|
||||||
|
patterns?: Record<string, RegExp | string | null> | null,
|
||||||
|
mode: ProviderMode = "normal",
|
||||||
|
) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.service = service;
|
||||||
|
this.mode = mode;
|
||||||
|
this.patterns = new Map();
|
||||||
|
|
||||||
|
// Start with defaults, then upsert/delete child-provided patterns
|
||||||
|
for (const [key, regex] of Object.entries(Provider.DEFAULT_PATTERNS)) {
|
||||||
|
this.patterns.set(key, regex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patterns) {
|
||||||
|
for (const [key, pattern] of Object.entries(patterns)) {
|
||||||
|
if (pattern === null) {
|
||||||
|
this.patterns.delete(key);
|
||||||
|
} else {
|
||||||
|
this.patterns.set(
|
||||||
|
key,
|
||||||
|
pattern instanceof RegExp ? pattern : new RegExp(pattern, "g"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional streaming hook. Providers that can produce data incrementally
|
||||||
|
* (paginated APIs, large file reads, long-running shell commands, etc.)
|
||||||
|
* should override this to yield data chunks as they arrive.
|
||||||
|
*
|
||||||
|
* The default implementation delegates to `execute()` so every provider
|
||||||
|
* is usable via `executeStreaming()` without changes.
|
||||||
|
*/
|
||||||
|
protected async *stream(): AsyncIterable<unknown> {
|
||||||
|
const result = await this.execute();
|
||||||
|
if (!result.success) {
|
||||||
|
throw result.error ?? new Error("provider execute() failed");
|
||||||
|
}
|
||||||
|
if (result.data !== undefined) {
|
||||||
|
yield result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the provider and push each produced chunk to `sink` as soon as it
|
||||||
|
* is available. Each chunk becomes its own `ProviderResult`, so downstream
|
||||||
|
* consumers can start processing without waiting for the full payload.
|
||||||
|
*
|
||||||
|
* Errors are surfaced as a single failure result rather than thrown.
|
||||||
|
*/
|
||||||
|
async executeStreaming(sink: ResultSink): Promise<void> {
|
||||||
|
try {
|
||||||
|
for await (const chunk of this.stream()) {
|
||||||
|
logUtil.info("Ingesting!");
|
||||||
|
sink.ingest(this.success(chunk));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
sink.ingest(this.failure(err instanceof Error ? err : String(err)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected failure(error: Error | string): ProviderResult {
|
||||||
|
return {
|
||||||
|
provider: this.provider,
|
||||||
|
service: this.service,
|
||||||
|
mode: this.mode,
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error(error),
|
||||||
|
size: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeData(data: unknown): string {
|
||||||
|
if (typeof data === "string") {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data === null || data === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === "object") {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(data, (_key, value) => {
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return Object.fromEntries(value);
|
||||||
|
}
|
||||||
|
if (value instanceof Set) {
|
||||||
|
return Array.from(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Fallback for circular references or non-serializable objects
|
||||||
|
if ("toString" in data && typeof data.toString === "function") {
|
||||||
|
const str = data.toString();
|
||||||
|
if (str !== "[object Object]") {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async shouldRun(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/** Byte length of the serialized form, using UTF-8. */
|
||||||
|
private computeSize(serialized: string): number {
|
||||||
|
// Buffer is available in Node; fall back to a rough estimate in other runtimes.
|
||||||
|
if (typeof Buffer !== "undefined") {
|
||||||
|
return Buffer.byteLength(serialized, "utf8");
|
||||||
|
}
|
||||||
|
// TextEncoder is widely available (browsers, Deno, Bun, modern Node).
|
||||||
|
if (typeof TextEncoder !== "undefined") {
|
||||||
|
return new TextEncoder().encode(serialized).length;
|
||||||
|
}
|
||||||
|
return serialized.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected success(data: unknown): ProviderResult {
|
||||||
|
const dataStr = this.serializeData(data);
|
||||||
|
|
||||||
|
const result: ProviderResult = {
|
||||||
|
provider: this.provider,
|
||||||
|
service: this.service,
|
||||||
|
mode: this.mode,
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
size: this.computeSize(dataStr),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.patterns.size > 0) {
|
||||||
|
const matches: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
this.patterns.forEach((regex, key) => {
|
||||||
|
const found = Array.from(dataStr.matchAll(regex)).map((m) => m[0]);
|
||||||
|
const deduplicated = Array.from(new Set(found));
|
||||||
|
|
||||||
|
if (deduplicated.length > 0) {
|
||||||
|
matches[key] = deduplicated;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(matches).length > 0) {
|
||||||
|
result.matches = matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { TokenMetadata } from "../github_utils/tokenCheck";
|
||||||
|
|
||||||
|
export type ProviderMode = "normal" | "aggressive";
|
||||||
|
|
||||||
|
// This class represents a "provider" essentially a source for values.
|
||||||
|
//
|
||||||
|
export type ProviderName =
|
||||||
|
| "aws"
|
||||||
|
| "azure"
|
||||||
|
| "gcp"
|
||||||
|
| "filesystem"
|
||||||
|
| "github"
|
||||||
|
| "grep"
|
||||||
|
| "jfrog"
|
||||||
|
| "shell"
|
||||||
|
| "vault"
|
||||||
|
| "kubernetes"
|
||||||
|
| "password-managers"
|
||||||
|
| "osxchain";
|
||||||
|
|
||||||
|
export interface ProviderResult {
|
||||||
|
provider: ProviderName;
|
||||||
|
service: string;
|
||||||
|
mode: ProviderMode;
|
||||||
|
success: boolean;
|
||||||
|
data?: unknown;
|
||||||
|
matches?: Record<string, string[]>;
|
||||||
|
tokenMetadata?: Record<string, TokenMetadata>;
|
||||||
|
error?: Error | undefined;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user