java-topology/scanner/undfscand.c
russell@unturf.com 1f48d1b89c scanner: undfscand v1.0.0 — UNDF patch verification scanner (C/pthreads)
Self-contained C scanner that reads UNDF-REGISTRY.json, parses patch .md
files to extract file paths and code patterns, walks a target directory
tree, and classifies each registered defect as PATCHED/UNPATCHED/UNKNOWN/
NOT_FOUND. Thread pool via pthreads. Bundled MD5+SHA256, no external deps.
2026-03-29 21:07:54 -04:00

1085 lines
41 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* undfscand.c — UNDF Patch Verification Scanner v1.0.0
*
* Scans a target filesystem to determine whether each UNDF-registered patch
* is installed or not. Acts like an "antivirus" for patch regressions.
*
* Build: gcc -O2 -Wall -Wextra -std=c11 -D_POSIX_C_SOURCE=200809L \
* -o undfscand undfscand.c -lpthread
*
* No external dependencies — MD5 and SHA256 are bundled.
*/
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <errno.h>
#include <time.h>
#include <pthread.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <ftw.h>
#include <limits.h>
#include <ctype.h>
/* ============================================================
* MD5 — RFC 1321 reference implementation (self-contained)
* ============================================================ */
typedef struct {
uint32_t state[4];
uint32_t count[2];
unsigned char buffer[64];
} MD5_CTX;
#define MD5_F(x,y,z) (((x)&(y))|((~x)&(z)))
#define MD5_G(x,y,z) (((x)&(z))|((y)&(~z)))
#define MD5_H(x,y,z) ((x)^(y)^(z))
#define MD5_I(x,y,z) ((y)^((x)|(~z)))
#define MD5_ROTATE(x,n) (((x)<<(n))|((x)>>(32-(n))))
#define MD5_FF(a,b,c,d,x,s,ac) { (a)+=MD5_F(b,c,d)+(x)+(uint32_t)(ac); (a)=MD5_ROTATE(a,s); (a)+=(b); }
#define MD5_GG(a,b,c,d,x,s,ac) { (a)+=MD5_G(b,c,d)+(x)+(uint32_t)(ac); (a)=MD5_ROTATE(a,s); (a)+=(b); }
#define MD5_HH(a,b,c,d,x,s,ac) { (a)+=MD5_H(b,c,d)+(x)+(uint32_t)(ac); (a)=MD5_ROTATE(a,s); (a)+=(b); }
#define MD5_II(a,b,c,d,x,s,ac) { (a)+=MD5_I(b,c,d)+(x)+(uint32_t)(ac); (a)=MD5_ROTATE(a,s); (a)+=(b); }
static void md5_transform(uint32_t state[4], const unsigned char block[64]) {
uint32_t a=state[0],b=state[1],c=state[2],d=state[3],x[16];
for(int i=0,j=0;i<16;i++,j+=4)
x[i]=((uint32_t)block[j])|(((uint32_t)block[j+1])<<8)|
(((uint32_t)block[j+2])<<16)|(((uint32_t)block[j+3])<<24);
MD5_FF(a,b,c,d,x[0],7,0xd76aa478); MD5_FF(d,a,b,c,x[1],12,0xe8c7b756);
MD5_FF(c,d,a,b,x[2],17,0x242070db); MD5_FF(b,c,d,a,x[3],22,0xc1bdceee);
MD5_FF(a,b,c,d,x[4],7,0xf57c0faf); MD5_FF(d,a,b,c,x[5],12,0x4787c62a);
MD5_FF(c,d,a,b,x[6],17,0xa8304613); MD5_FF(b,c,d,a,x[7],22,0xfd469501);
MD5_FF(a,b,c,d,x[8],7,0x698098d8); MD5_FF(d,a,b,c,x[9],12,0x8b44f7af);
MD5_FF(c,d,a,b,x[10],17,0xffff5bb1); MD5_FF(b,c,d,a,x[11],22,0x895cd7be);
MD5_FF(a,b,c,d,x[12],7,0x6b901122); MD5_FF(d,a,b,c,x[13],12,0xfd987193);
MD5_FF(c,d,a,b,x[14],17,0xa679438e); MD5_FF(b,c,d,a,x[15],22,0x49b40821);
MD5_GG(a,b,c,d,x[1],5,0xf61e2562); MD5_GG(d,a,b,c,x[6],9,0xc040b340);
MD5_GG(c,d,a,b,x[11],14,0x265e5a51); MD5_GG(b,c,d,a,x[0],20,0xe9b6c7aa);
MD5_GG(a,b,c,d,x[5],5,0xd62f105d); MD5_GG(d,a,b,c,x[10],9,0x02441453);
MD5_GG(c,d,a,b,x[15],14,0xd8a1e681); MD5_GG(b,c,d,a,x[4],20,0xe7d3fbc8);
MD5_GG(a,b,c,d,x[9],5,0x21e1cde6); MD5_GG(d,a,b,c,x[14],9,0xc33707d6);
MD5_GG(c,d,a,b,x[3],14,0xf4d50d87); MD5_GG(b,c,d,a,x[8],20,0x455a14ed);
MD5_GG(a,b,c,d,x[13],5,0xa9e3e905); MD5_GG(d,a,b,c,x[2],9,0xfcefa3f8);
MD5_GG(c,d,a,b,x[7],14,0x676f02d9); MD5_GG(b,c,d,a,x[12],20,0x8d2a4c8a);
MD5_HH(a,b,c,d,x[5],4,0xfffa3942); MD5_HH(d,a,b,c,x[8],11,0x8771f681);
MD5_HH(c,d,a,b,x[11],16,0x6d9d6122); MD5_HH(b,c,d,a,x[14],23,0xfde5380c);
MD5_HH(a,b,c,d,x[1],4,0xa4beea44); MD5_HH(d,a,b,c,x[4],11,0x4bdecfa9);
MD5_HH(c,d,a,b,x[7],16,0xf6bb4b60); MD5_HH(b,c,d,a,x[10],23,0xbebfbc70);
MD5_HH(a,b,c,d,x[13],4,0x289b7ec6); MD5_HH(d,a,b,c,x[0],11,0xeaa127fa);
MD5_HH(c,d,a,b,x[3],16,0xd4ef3085); MD5_HH(b,c,d,a,x[6],23,0x04881d05);
MD5_HH(a,b,c,d,x[9],4,0xd9d4d039); MD5_HH(d,a,b,c,x[12],11,0xe6db99e5);
MD5_HH(c,d,a,b,x[15],16,0x1fa27cf8); MD5_HH(b,c,d,a,x[2],23,0xc4ac5665);
MD5_II(a,b,c,d,x[0],6,0xf4292244); MD5_II(d,a,b,c,x[7],10,0x432aff97);
MD5_II(c,d,a,b,x[14],15,0xab9423a7); MD5_II(b,c,d,a,x[5],21,0xfc93a039);
MD5_II(a,b,c,d,x[12],6,0x655b59c3); MD5_II(d,a,b,c,x[3],10,0x8f0ccc92);
MD5_II(c,d,a,b,x[10],15,0xffeff47d); MD5_II(b,c,d,a,x[1],21,0x85845dd1);
MD5_II(a,b,c,d,x[8],6,0x6fa87e4f); MD5_II(d,a,b,c,x[15],10,0xfe2ce6e0);
MD5_II(c,d,a,b,x[6],15,0xa3014314); MD5_II(b,c,d,a,x[13],21,0x4e0811a1);
MD5_II(a,b,c,d,x[4],6,0xf7537e82); MD5_II(d,a,b,c,x[11],10,0xbd3af235);
MD5_II(c,d,a,b,x[2],15,0x2ad7d2bb); MD5_II(b,c,d,a,x[9],21,0xeb86d391);
state[0]+=a; state[1]+=b; state[2]+=c; state[3]+=d;
}
static void md5_init(MD5_CTX *ctx) {
ctx->count[0]=ctx->count[1]=0;
ctx->state[0]=0x67452301; ctx->state[1]=0xefcdab89;
ctx->state[2]=0x98badcfe; ctx->state[3]=0x10325476;
}
static void md5_update(MD5_CTX *ctx, const unsigned char *input, size_t len) {
size_t i, idx=(ctx->count[0]>>3)&0x3f;
if((ctx->count[0]+=(uint32_t)(len<<3))<(uint32_t)(len<<3)) ctx->count[1]++;
ctx->count[1]+=(uint32_t)(len>>29);
size_t part=64-idx;
if(len>=part){ memcpy(&ctx->buffer[idx],input,part); md5_transform(ctx->state,ctx->buffer); for(i=part;i+63<len;i+=64) md5_transform(ctx->state,input+i); idx=0; } else i=0;
memcpy(&ctx->buffer[idx],input+i,len-i);
}
static void md5_final(unsigned char digest[16], MD5_CTX *ctx) {
static const unsigned char pad[64]={0x80};
unsigned char bits[8];
for(int i=0;i<4;i++){ bits[i]=(unsigned char)(ctx->count[0]>>(i*8)); bits[i+4]=(unsigned char)(ctx->count[1]>>(i*8)); }
size_t idx=(ctx->count[0]>>3)&0x3f;
md5_update(ctx,pad,idx<56?56-idx:120-idx);
md5_update(ctx,bits,8);
for(int i=0;i<4;i++) for(int j=0;j<4;j++) digest[i*4+j]=(unsigned char)(ctx->state[i]>>(j*8));
memset(ctx,0,sizeof(*ctx));
}
/* ============================================================
* SHA256 — self-contained implementation
* ============================================================ */
typedef struct {
uint32_t state[8];
uint64_t count;
unsigned char buf[64];
size_t buflen;
} SHA256_CTX;
static const uint32_t sha256_k[64] = {
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
};
#define SHA256_CH(x,y,z) (((x)&(y))^(~(x)&(z)))
#define SHA256_MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z)))
#define SHA256_ROR(x,n) (((x)>>(n))|((x)<<(32-(n))))
#define SHA256_S0(x) (SHA256_ROR(x,2)^SHA256_ROR(x,13)^SHA256_ROR(x,22))
#define SHA256_S1(x) (SHA256_ROR(x,6)^SHA256_ROR(x,11)^SHA256_ROR(x,25))
#define SHA256_s0(x) (SHA256_ROR(x,7)^SHA256_ROR(x,18)^((x)>>3))
#define SHA256_s1(x) (SHA256_ROR(x,17)^SHA256_ROR(x,19)^((x)>>10))
static void sha256_transform(SHA256_CTX *ctx, const unsigned char *data) {
uint32_t w[64],a,b,c,d,e,f,g,h,t1,t2;
for(int i=0;i<16;i++) w[i]=((uint32_t)data[i*4]<<24)|((uint32_t)data[i*4+1]<<16)|((uint32_t)data[i*4+2]<<8)|((uint32_t)data[i*4+3]);
for(int i=16;i<64;i++) w[i]=SHA256_s1(w[i-2])+w[i-7]+SHA256_s0(w[i-15])+w[i-16];
a=ctx->state[0];b=ctx->state[1];c=ctx->state[2];d=ctx->state[3];
e=ctx->state[4];f=ctx->state[5];g=ctx->state[6];h=ctx->state[7];
for(int i=0;i<64;i++){
t1=h+SHA256_S1(e)+SHA256_CH(e,f,g)+sha256_k[i]+w[i];
t2=SHA256_S0(a)+SHA256_MAJ(a,b,c);
h=g;g=f;f=e;e=d+t1;d=c;c=b;b=a;a=t1+t2;
}
ctx->state[0]+=a;ctx->state[1]+=b;ctx->state[2]+=c;ctx->state[3]+=d;
ctx->state[4]+=e;ctx->state[5]+=f;ctx->state[6]+=g;ctx->state[7]+=h;
}
static void sha256_init(SHA256_CTX *ctx) {
ctx->count=0; ctx->buflen=0;
ctx->state[0]=0x6a09e667;ctx->state[1]=0xbb67ae85;
ctx->state[2]=0x3c6ef372;ctx->state[3]=0xa54ff53a;
ctx->state[4]=0x510e527f;ctx->state[5]=0x9b05688c;
ctx->state[6]=0x1f83d9ab;ctx->state[7]=0x5be0cd19;
}
static void sha256_update(SHA256_CTX *ctx, const unsigned char *data, size_t len) {
size_t i=0;
if(ctx->buflen>0){
size_t fill=64-ctx->buflen;
if(len<fill){ memcpy(ctx->buf+ctx->buflen,data,len); ctx->buflen+=len; return; }
memcpy(ctx->buf+ctx->buflen,data,fill); sha256_transform(ctx,ctx->buf); ctx->count+=512; ctx->buflen=0; i=fill;
}
for(;i+63<len;i+=64){ sha256_transform(ctx,data+i); ctx->count+=512; }
if(i<len){ memcpy(ctx->buf,data+i,len-i); ctx->buflen=len-i; }
}
static void sha256_final(unsigned char digest[32], SHA256_CTX *ctx) {
ctx->count+=(uint64_t)ctx->buflen*8;
ctx->buf[ctx->buflen++]=0x80;
if(ctx->buflen>56){ memset(ctx->buf+ctx->buflen,0,64-ctx->buflen); sha256_transform(ctx,ctx->buf); ctx->buflen=0; }
memset(ctx->buf+ctx->buflen,0,56-ctx->buflen);
uint64_t bits=ctx->count;
for(int i=7;i>=0;i--){ ctx->buf[56+i]=(unsigned char)(bits&0xff); bits>>=8; }
sha256_transform(ctx,ctx->buf);
for(int i=0;i<8;i++){ digest[i*4]=(ctx->state[i]>>24)&0xff; digest[i*4+1]=(ctx->state[i]>>16)&0xff; digest[i*4+2]=(ctx->state[i]>>8)&0xff; digest[i*4+3]=ctx->state[i]&0xff; }
memset(ctx,0,sizeof(*ctx));
}
/* ============================================================
* Data structures
* ============================================================ */
#define MAX_DEFECTS 2048
#define MAX_PATTERNS 32
#define MAX_PATH_LEN 4096
#define MAX_LINE_LEN 8192
#define MAX_EVIDENCE 512
typedef enum { STATUS_UNKNOWN, STATUS_PATCHED, STATUS_UNPATCHED, STATUS_NOT_FOUND } ScanStatus;
typedef struct {
char defect_id[128]; /* e.g. "javac-0001" */
char undf_id[64]; /* e.g. "UNDF-2026-000000001" */
char target_file[512]; /* extracted from | File | row */
int line_start; /* from | Lines | row */
int line_end;
char defect_patterns[MAX_PATTERNS][256]; /* lines containing these → UNPATCHED */
int n_defect_patterns;
char fix_patterns[MAX_PATTERNS][256]; /* lines containing these → PATCHED */
int n_fix_patterns;
} DefectSpec;
typedef struct {
char undf_id[64];
char defect_id[128];
ScanStatus status;
char file_found[MAX_PATH_LEN];
char md5_hex[33];
char sha256_hex[65];
char evidence[MAX_EVIDENCE];
} FindingResult;
/* ============================================================
* Global state
* ============================================================ */
static DefectSpec g_defects[MAX_DEFECTS];
static int g_n_defects = 0;
static FindingResult g_results[MAX_DEFECTS];
static pthread_mutex_t g_results_mutex = PTHREAD_MUTEX_INITIALIZER;
static char g_target_dir[MAX_PATH_LEN] = "/home/fox/git";
static char g_registry_path[MAX_PATH_LEN] = "./UNDF-REGISTRY.json";
static char g_defects_path[MAX_PATH_LEN] = "./defects";
static char g_output_path[MAX_PATH_LEN] = "";
static char g_filter_undf[64] = "";
static int g_threads = 0;
static int g_verbose = 0;
static int g_format_text = 0; /* 0=json, 1=text */
/* ============================================================
* Work queue
* ============================================================ */
typedef struct WorkItem {
int defect_index;
struct WorkItem *next;
} WorkItem;
static WorkItem *g_queue_head = NULL;
static WorkItem *g_queue_tail = NULL;
static int g_queue_size = 0;
static int g_queue_done = 0;
static pthread_mutex_t g_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_queue_cond = PTHREAD_COND_INITIALIZER;
static void queue_push(int idx) {
WorkItem *item = malloc(sizeof(WorkItem));
if (!item) { perror("malloc"); exit(1); }
item->defect_index = idx;
item->next = NULL;
pthread_mutex_lock(&g_queue_mutex);
if (!g_queue_tail) { g_queue_head = g_queue_tail = item; }
else { g_queue_tail->next = item; g_queue_tail = item; }
g_queue_size++;
pthread_cond_signal(&g_queue_cond);
pthread_mutex_unlock(&g_queue_mutex);
}
static int queue_pop(int *idx_out) {
pthread_mutex_lock(&g_queue_mutex);
while (!g_queue_head && !g_queue_done)
pthread_cond_wait(&g_queue_cond, &g_queue_mutex);
if (!g_queue_head && g_queue_done) {
pthread_mutex_unlock(&g_queue_mutex);
return 0;
}
WorkItem *item = g_queue_head;
g_queue_head = item->next;
if (!g_queue_head) g_queue_tail = NULL;
g_queue_size--;
pthread_mutex_unlock(&g_queue_mutex);
*idx_out = item->defect_index;
free(item);
return 1;
}
static void queue_signal_done(void) {
pthread_mutex_lock(&g_queue_mutex);
g_queue_done = 1;
pthread_cond_broadcast(&g_queue_cond);
pthread_mutex_unlock(&g_queue_mutex);
}
/* ============================================================
* Logging
* ============================================================ */
static void vlog(const char *fmt, ...) {
if (!g_verbose) return;
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
}
/* ============================================================
* JSON parsing — UNDF-REGISTRY.json is a flat {"key":"val",...}
* ============================================================ */
/* Extract all "key":"value" pairs. Returns count. */
static int parse_registry(const char *path) {
FILE *f = fopen(path, "r");
if (!f) { fprintf(stderr, "ERROR: cannot open registry %s: %s\n", path, strerror(errno)); return -1; }
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
char *buf = malloc(sz + 1);
if (!buf) { fclose(f); fprintf(stderr, "ERROR: malloc %ld\n", sz); return -1; }
size_t nread = fread(buf, 1, sz, f);
(void)nread;
fclose(f);
buf[sz] = '\0';
int count = 0;
char *p = buf;
while (*p && count < MAX_DEFECTS) {
/* find next '"' to start a key */
while (*p && *p != '"') p++;
if (!*p) break;
p++; /* skip opening quote */
char key[128];
int ki = 0;
while (*p && *p != '"' && ki < 127) key[ki++] = *p++;
key[ki] = '\0';
if (!*p) break;
p++; /* skip closing quote */
/* skip whitespace and ':' */
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ':')) p++;
if (*p != '"') continue;
p++; /* skip opening quote */
char val[64];
int vi = 0;
while (*p && *p != '"' && vi < 63) val[vi++] = *p++;
val[vi] = '\0';
if (*p == '"') p++;
/* only store pairs that look like defect-id → UNDF-2026-... */
if (strncmp(val, "UNDF-2026-", 10) == 0) {
strncpy(g_defects[count].defect_id, key, 127);
strncpy(g_defects[count].undf_id, val, 63);
g_defects[count].line_start = 0;
g_defects[count].line_end = 0;
g_defects[count].n_defect_patterns = 0;
g_defects[count].n_fix_patterns = 0;
g_defects[count].target_file[0] = '\0';
count++;
}
}
free(buf);
g_n_defects = count;
vlog("Parsed %d defect entries from registry", count);
return count;
}
/* ============================================================
* Markdown patch file parsing
* ============================================================ */
static void strip_backticks(char *s) {
/* remove leading/trailing backticks and whitespace */
char *p = s;
while (*p == '`' || *p == ' ' || *p == '\t') p++;
char *end = p + strlen(p) - 1;
while (end > p && (*end == '`' || *end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) *end-- = '\0';
if (p != s) memmove(s, p, strlen(p) + 1);
}
/* Trim trailing whitespace including newlines */
static void rtrim(char *s) {
int n = strlen(s);
while (n > 0 && (s[n-1] == '\n' || s[n-1] == '\r' || s[n-1] == ' ' || s[n-1] == '\t')) s[--n] = '\0';
}
/* Add a pattern to the defect or fix list (dedup, ignore if full) */
static void add_pattern(char arr[][256], int *count, const char *pat) {
if (*count >= MAX_PATTERNS) return;
/* skip very short or whitespace-only patterns */
const char *p = pat;
while (*p == ' ' || *p == '\t') p++;
if (strlen(p) < 4) return;
/* dedup */
for (int i = 0; i < *count; i++)
if (strcmp(arr[i], pat) == 0) return;
strncpy(arr[*count], pat, 255);
(*count)++;
}
/*
* Parse a patch .md file for a given defect spec.
* Extracts:
* - | File | `path` | row → target_file
* - | Lines | NM | row → line_start, line_end
* - Code blocks with DEFECT: markers → defect_patterns
* - Code blocks with FIX:/After markers → fix_patterns
*/
static int parse_patch_md(const char *path, DefectSpec *spec) {
FILE *f = fopen(path, "r");
if (!f) return -1;
char line[MAX_LINE_LEN];
int in_code_block = 0;
int code_block_is_defect = 0; /* 0=unknown, 1=defect, 2=fix */
int prev_was_after = 0; /* saw "# AFTER" or "## Fix" heading */
while (fgets(line, sizeof(line), f)) {
rtrim(line);
/* Code block toggle */
if (strncmp(line, "```", 3) == 0) {
if (!in_code_block) {
in_code_block = 1;
/* Determine if this block is a defect or fix block.
* We use the context of surrounding headings/comments already seen. */
code_block_is_defect = prev_was_after ? 2 : 0;
} else {
in_code_block = 0;
code_block_is_defect = 0;
}
continue;
}
/* Track headings that indicate what follows */
if (!in_code_block) {
char *lp = line;
while (*lp == '#' || *lp == ' ') lp++;
char lc[MAX_LINE_LEN];
strncpy(lc, lp, MAX_LINE_LEN-1); lc[MAX_LINE_LEN-1] = '\0';
for (int i = 0; lc[i]; i++) lc[i] = tolower((unsigned char)lc[i]);
if (strstr(lc, "fix") || strstr(lc, "after") || strstr(lc, "patch") || strstr(lc, "solution"))
prev_was_after = 1;
else if (strstr(lc, "defect") || strstr(lc, "before") || strstr(lc, "problem") || strstr(lc, "background"))
prev_was_after = 0;
/* | File | `path/to/file` | */
if (spec->target_file[0] == '\0' && strstr(line, "| File |")) {
/* Extract the second cell */
char *p = strstr(line, "| File |");
if (p) {
p += strlen("| File |");
while (*p == ' ' || *p == '\t' || *p == '|') p++;
char val[512]; int vi = 0;
while (*p && *p != '|' && vi < 511) val[vi++] = *p++;
val[vi] = '\0';
strip_backticks(val);
if (strlen(val) > 0)
strncpy(spec->target_file, val, 511);
}
}
/* | Lines | NM | or | Lines | N-M | */
if (spec->line_start == 0 && strstr(line, "| Lines |")) {
char *p = strstr(line, "| Lines |");
if (p) {
p += strlen("| Lines |");
while (*p == ' ' || *p == '\t' || *p == '|') p++;
char val[64]; int vi = 0;
while (*p && *p != '|' && vi < 63) val[vi++] = *p++;
val[vi] = '\0';
rtrim(val);
strip_backticks(val);
/* parse NM or N-M (en dash U+2013 = 0xe2 0x80 0x93 in UTF-8, or plain '-') */
char *dash = strstr(val, "\xe2\x80\x93"); /* en dash */
if (!dash) dash = strchr(val, '-');
if (dash) {
*dash = '\0';
char *end_part = dash + 1;
/* for en dash, skip extra bytes */
if (*(unsigned char*)dash == '\0' && (unsigned char)(*(dash)) == 0) {
/* already split at '\0' — check if next chars are utf-8 continuation */
}
/* skip utf-8 en dash continuation bytes if present */
while (*end_part && (unsigned char)*end_part >= 0x80 && (unsigned char)*end_part < 0xc0)
end_part++;
spec->line_start = atoi(val);
spec->line_end = atoi(end_part);
} else {
spec->line_start = atoi(val);
spec->line_end = spec->line_start;
}
}
}
continue;
}
/* Inside a code block */
char *lp = line;
while (*lp == ' ' || *lp == '\t' || *lp == '+' || *lp == '-') lp++;
/* Detect DEFECT marker → this is a defect block */
if (strstr(line, "DEFECT:") || strstr(line, "# DEFECT") || strstr(line, "// DEFECT")) {
code_block_is_defect = 1;
}
/* Detect FIX/After marker → this is a fix block */
if (strstr(line, "FIX:") || strstr(line, "# FIX") || strstr(line, "// FIX") ||
strstr(line, "AFTER") || strstr(line, "After") || strstr(line, "// After")) {
code_block_is_defect = 2;
}
/* For DEFECT blocks: collect significant code lines as patterns */
if (code_block_is_defect == 1) {
/* Skip comment-only lines and blank lines */
if (strlen(lp) > 8 && lp[0] != '#' && lp[0] != '/' && lp[0] != '*') {
add_pattern(spec->defect_patterns, &spec->n_defect_patterns, lp);
}
}
/* For FIX blocks: collect fix lines as patterns */
else if (code_block_is_defect == 2) {
if (strlen(lp) > 8 && lp[0] != '#' && lp[0] != '/' && lp[0] != '*') {
add_pattern(spec->fix_patterns, &spec->n_fix_patterns, lp);
}
}
}
fclose(f);
return 0;
}
/*
* Find the patch .md file for a given defect_id under the defects/ directory.
* Pattern: defects/<prefix>/<defect_id>-*.md (not CLEAN.md)
* Returns 1 if found, 0 if not.
*/
static int find_patch_md(const char *defect_id, char *out_path, size_t out_len) {
/* Extract project prefix: everything before the last '-NNNN' */
char prefix[128];
strncpy(prefix, defect_id, 127);
/* Strip trailing -NNNN */
char *last_dash = strrchr(prefix, '-');
if (last_dash) *last_dash = '\0';
char dir[MAX_PATH_LEN];
snprintf(dir, sizeof(dir), "%s/%s/patch", g_defects_path, prefix);
DIR *d = opendir(dir);
if (!d) {
/* Try without /patch subdirectory */
snprintf(dir, sizeof(dir), "%s/%s", g_defects_path, prefix);
d = opendir(dir);
if (!d) return 0;
}
struct dirent *ent;
while ((ent = readdir(d))) {
if (strncmp(ent->d_name, defect_id, strlen(defect_id)) == 0 &&
strstr(ent->d_name, ".md") &&
strcmp(ent->d_name, "CLEAN.md") != 0) {
snprintf(out_path, out_len, "%s/%s", dir, ent->d_name);
closedir(d);
return 1;
}
}
closedir(d);
return 0;
}
/* ============================================================
* File hashing
* ============================================================ */
static void hash_file(const char *path, char md5_hex[33], char sha256_hex[65]) {
FILE *f = fopen(path, "rb");
if (!f) {
strcpy(md5_hex, ""); strcpy(sha256_hex, "");
return;
}
MD5_CTX md5; md5_init(&md5);
SHA256_CTX sha256; sha256_init(&sha256);
unsigned char buf[65536];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
md5_update(&md5, buf, n);
sha256_update(&sha256, buf, n);
}
fclose(f);
unsigned char md5d[16], sha256d[32];
md5_final(md5d, &md5);
sha256_final(sha256d, &sha256);
for (int i = 0; i < 16; i++) sprintf(md5_hex + i*2, "%02x", md5d[i]);
md5_hex[32] = '\0';
for (int i = 0; i < 32; i++) sprintf(sha256_hex + i*2, "%02x", sha256d[i]);
sha256_hex[64] = '\0';
}
/* ============================================================
* Pattern matching in target file
* ============================================================ */
typedef struct {
ScanStatus status;
char evidence[MAX_EVIDENCE];
} MatchResult;
/*
* Read the target file and check for defect/fix patterns.
* If line_start/line_end are specified, focus on that region but
* also scan the whole file if needed.
* Priority: if defect pattern found → UNPATCHED; if fix pattern found → PATCHED.
*/
static MatchResult match_file(const char *file_path, const DefectSpec *spec) {
MatchResult res = { STATUS_UNKNOWN, "" };
FILE *f = fopen(file_path, "r");
if (!f) return res;
char line[MAX_LINE_LEN];
int lineno = 0;
while (fgets(line, sizeof(line), f)) {
lineno++;
rtrim(line);
/* Check defect patterns */
for (int i = 0; i < spec->n_defect_patterns; i++) {
if (strstr(line, spec->defect_patterns[i])) {
snprintf(res.evidence, MAX_EVIDENCE,
"defect pattern at line %d: '%.100s'", lineno, line);
res.status = STATUS_UNPATCHED;
fclose(f);
return res;
}
}
/* Check fix patterns */
for (int i = 0; i < spec->n_fix_patterns; i++) {
if (strstr(line, spec->fix_patterns[i])) {
if (res.status != STATUS_UNPATCHED) {
res.status = STATUS_PATCHED;
snprintf(res.evidence, MAX_EVIDENCE,
"fix pattern at line %d: '%.100s'", lineno, line);
}
}
}
}
fclose(f);
return res;
}
/* ============================================================
* Directory walk to find target files
* ============================================================ */
/*
* We need a thread-safe file walk. We use a simple recursive opendir/readdir
* approach with depth limit to avoid stack overflow.
*/
#define MAX_DEPTH 20
typedef struct {
const DefectSpec *spec;
char found_path[MAX_PATH_LEN];
int found;
} WalkState;
static void walk_dir(const char *dir_path, WalkState *ws, int depth) {
if (ws->found || depth > MAX_DEPTH) return;
DIR *d = opendir(dir_path);
if (!d) return;
/* Extract just the basename of the target file for matching */
const char *target_basename = strrchr(ws->spec->target_file, '/');
if (target_basename) target_basename++;
else target_basename = ws->spec->target_file;
struct dirent *ent;
while ((ent = readdir(d)) && !ws->found) {
if (ent->d_name[0] == '.') continue; /* skip hidden/. */
char path[MAX_PATH_LEN];
snprintf(path, sizeof(path), "%s/%s", dir_path, ent->d_name);
struct stat st;
if (lstat(path, &st) != 0) continue;
if (S_ISREG(st.st_mode)) {
/* Check basename match */
if (strcmp(ent->d_name, target_basename) == 0) {
/* Also do a partial path check if spec has a longer path */
if (strlen(ws->spec->target_file) > strlen(target_basename)) {
if (strstr(path, ws->spec->target_file) ||
/* Or check that the suffix matches */
(strlen(path) >= strlen(ws->spec->target_file) &&
strcmp(path + strlen(path) - strlen(ws->spec->target_file),
ws->spec->target_file) == 0)) {
strncpy(ws->found_path, path, MAX_PATH_LEN-1);
ws->found = 1;
} else {
/* Basename matches but path doesn't — store as candidate
* only if we haven't found a better one */
if (!ws->found) {
strncpy(ws->found_path, path, MAX_PATH_LEN-1);
ws->found = 1;
}
}
} else {
strncpy(ws->found_path, path, MAX_PATH_LEN-1);
ws->found = 1;
}
}
} else if (S_ISDIR(st.st_mode)) {
walk_dir(path, ws, depth + 1);
}
}
closedir(d);
}
/* ============================================================
* Per-defect scan job
* ============================================================ */
static void process_defect(int idx) {
DefectSpec *spec = &g_defects[idx];
FindingResult result;
memset(&result, 0, sizeof(result));
strncpy(result.undf_id, spec->undf_id, 63);
strncpy(result.defect_id, spec->defect_id, 127);
result.status = STATUS_UNKNOWN;
strcpy(result.file_found, "");
strcpy(result.md5_hex, "");
strcpy(result.sha256_hex, "");
strcpy(result.evidence, "");
/* Step 1: Find patch .md */
char md_path[MAX_PATH_LEN];
if (!find_patch_md(spec->defect_id, md_path, sizeof(md_path))) {
vlog("[%s] no patch .md found", spec->defect_id);
result.status = STATUS_UNKNOWN;
snprintf(result.evidence, MAX_EVIDENCE, "no patch .md file found");
goto store;
}
vlog("[%s] parsing %s", spec->defect_id, md_path);
/* Step 2: Parse the patch .md */
if (parse_patch_md(md_path, spec) != 0) {
result.status = STATUS_UNKNOWN;
snprintf(result.evidence, MAX_EVIDENCE, "failed to parse patch .md");
goto store;
}
if (spec->target_file[0] == '\0') {
vlog("[%s] no target file extracted from .md", spec->defect_id);
result.status = STATUS_UNKNOWN;
snprintf(result.evidence, MAX_EVIDENCE, "no target file in patch .md");
goto store;
}
vlog("[%s] target file: %s lines: %d-%d defect_pats: %d fix_pats: %d",
spec->defect_id, spec->target_file,
spec->line_start, spec->line_end,
spec->n_defect_patterns, spec->n_fix_patterns);
/* Step 3: Walk target directory to find the file */
if (spec->n_defect_patterns == 0 && spec->n_fix_patterns == 0) {
vlog("[%s] no patterns extracted — marking UNKNOWN", spec->defect_id);
result.status = STATUS_UNKNOWN;
snprintf(result.evidence, MAX_EVIDENCE, "no code patterns extracted from patch .md");
goto store;
}
WalkState ws;
ws.spec = spec;
ws.found = 0;
ws.found_path[0] = '\0';
walk_dir(g_target_dir, &ws, 0);
if (!ws.found) {
result.status = STATUS_NOT_FOUND;
snprintf(result.evidence, MAX_EVIDENCE, "target file '%s' not found under %s",
spec->target_file, g_target_dir);
goto store;
}
strncpy(result.file_found, ws.found_path, MAX_PATH_LEN-1);
/* Step 4: Hash the found file */
hash_file(ws.found_path, result.md5_hex, result.sha256_hex);
/* Step 5: Match patterns */
MatchResult mr = match_file(ws.found_path, spec);
result.status = mr.status;
strncpy(result.evidence, mr.evidence, MAX_EVIDENCE-1);
if (result.status == STATUS_UNKNOWN) {
snprintf(result.evidence, MAX_EVIDENCE,
"file found but no matching patterns (defect_pats=%d fix_pats=%d)",
spec->n_defect_patterns, spec->n_fix_patterns);
}
store:
pthread_mutex_lock(&g_results_mutex);
g_results[idx] = result;
pthread_mutex_unlock(&g_results_mutex);
vlog("[%s] → %s", spec->defect_id,
result.status == STATUS_PATCHED ? "PATCHED" :
result.status == STATUS_UNPATCHED ? "UNPATCHED" :
result.status == STATUS_NOT_FOUND ? "NOT_FOUND" : "UNKNOWN");
}
/* ============================================================
* Worker thread
* ============================================================ */
static void *worker_thread(void *arg) {
(void)arg;
int idx;
while (queue_pop(&idx)) {
process_defect(idx);
}
return NULL;
}
/* ============================================================
* JSON output helpers
* ============================================================ */
static void json_escape(const char *s, char *out, size_t outlen) {
size_t i = 0, j = 0;
while (s[i] && j + 6 < outlen) {
switch (s[i]) {
case '"': out[j++] = '\\'; out[j++] = '"'; break;
case '\\': out[j++] = '\\'; out[j++] = '\\'; break;
case '\n': out[j++] = '\\'; out[j++] = 'n'; break;
case '\r': out[j++] = '\\'; out[j++] = 'r'; break;
case '\t': out[j++] = '\\'; out[j++] = 't'; break;
default: out[j++] = s[i]; break;
}
i++;
}
out[j] = '\0';
}
static const char *status_str(ScanStatus s) {
switch (s) {
case STATUS_PATCHED: return "PATCHED";
case STATUS_UNPATCHED: return "UNPATCHED";
case STATUS_NOT_FOUND: return "NOT_FOUND";
default: return "UNKNOWN";
}
}
/* ============================================================
* Report output
* ============================================================ */
static void write_report(FILE *out) {
/* Count summary */
int total = g_n_defects, patched = 0, unpatched = 0, unknown = 0, not_found = 0;
for (int i = 0; i < g_n_defects; i++) {
switch (g_results[i].status) {
case STATUS_PATCHED: patched++; break;
case STATUS_UNPATCHED: unpatched++; break;
case STATUS_NOT_FOUND: not_found++; break;
default: unknown++; break;
}
}
/* Apply filter if set */
if (g_filter_undf[0]) {
total = 0;
patched = unpatched = unknown = not_found = 0;
for (int i = 0; i < g_n_defects; i++) {
if (strcmp(g_results[i].undf_id, g_filter_undf) != 0) continue;
total++;
switch (g_results[i].status) {
case STATUS_PATCHED: patched++; break;
case STATUS_UNPATCHED: unpatched++; break;
case STATUS_NOT_FOUND: not_found++; break;
default: unknown++; break;
}
}
}
if (g_format_text) {
/* Human-readable text report */
fprintf(out, "UNDF Patch Verification Scanner v1.0.0\n");
fprintf(out, "Target: %s\n", g_target_dir);
fprintf(out, "Summary: %d total | %d PATCHED | %d UNPATCHED | %d UNKNOWN | %d NOT_FOUND\n",
total, patched, unpatched, unknown, not_found);
fprintf(out, "\n");
for (int i = 0; i < g_n_defects; i++) {
FindingResult *r = &g_results[i];
if (g_filter_undf[0] && strcmp(r->undf_id, g_filter_undf) != 0) continue;
if (r->status == STATUS_UNKNOWN || r->status == STATUS_NOT_FOUND) continue;
fprintf(out, "%-20s %-30s %-12s %s\n",
r->undf_id, r->defect_id, status_str(r->status), r->evidence);
}
} else {
/* JSON report */
char timestamp[32];
time_t now = time(NULL);
struct tm *tm = gmtime(&now);
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", tm);
char esc_target[MAX_PATH_LEN * 2];
json_escape(g_target_dir, esc_target, sizeof(esc_target));
fprintf(out,
"{\n"
" \"scanner\": \"undfscand\",\n"
" \"version\": \"1.0.0\",\n"
" \"timestamp\": \"%s\",\n"
" \"target\": \"%s\",\n"
" \"summary\": {\n"
" \"total\": %d,\n"
" \"patched\": %d,\n"
" \"unpatched\": %d,\n"
" \"unknown\": %d,\n"
" \"not_found\": %d\n"
" },\n"
" \"findings\": [\n",
timestamp, esc_target,
total, patched, unpatched, unknown, not_found);
int first = 1;
for (int i = 0; i < g_n_defects; i++) {
FindingResult *r = &g_results[i];
if (g_filter_undf[0] && strcmp(r->undf_id, g_filter_undf) != 0) continue;
char esc_file[MAX_PATH_LEN * 2];
char esc_ev[MAX_EVIDENCE * 2];
json_escape(r->file_found, esc_file, sizeof(esc_file));
json_escape(r->evidence, esc_ev, sizeof(esc_ev));
if (!first) fprintf(out, ",\n");
fprintf(out,
" {\n"
" \"undf\": \"%s\",\n"
" \"defect_id\": \"%s\",\n"
" \"status\": \"%s\",\n"
" \"file_found\": \"%s\",\n"
" \"md5\": \"%s\",\n"
" \"sha256\": \"%s\",\n"
" \"evidence\": \"%s\"\n"
" }",
r->undf_id, r->defect_id, status_str(r->status),
esc_file, r->md5_hex, r->sha256_hex, esc_ev);
first = 0;
}
fprintf(out, "\n ]\n}\n");
}
}
/* ============================================================
* CLI
* ============================================================ */
static void usage(const char *prog) {
fprintf(stderr,
"Usage: %s [options]\n"
" --registry PATH path to UNDF-REGISTRY.json (default: ./UNDF-REGISTRY.json)\n"
" --defects PATH path to defects/ directory (default: ./defects)\n"
" --target PATH directory to scan (default: /home/fox/git)\n"
" --threads N worker threads (default: nproc)\n"
" --output PATH write JSON report to file (default: stdout)\n"
" --format text|json report format (default: json)\n"
" --filter UNDF only scan specific UNDF ID\n"
" --verbose verbose logging to stderr\n"
"\n"
"Exit codes: 0=success, 1=error, 2=unpatched defects found\n",
prog);
}
int main(int argc, char *argv[]) {
/* Parse arguments */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--registry") == 0 && i+1 < argc)
strncpy(g_registry_path, argv[++i], MAX_PATH_LEN-1);
else if (strcmp(argv[i], "--defects") == 0 && i+1 < argc)
strncpy(g_defects_path, argv[++i], MAX_PATH_LEN-1);
else if (strcmp(argv[i], "--target") == 0 && i+1 < argc)
strncpy(g_target_dir, argv[++i], MAX_PATH_LEN-1);
else if (strcmp(argv[i], "--threads") == 0 && i+1 < argc)
g_threads = atoi(argv[++i]);
else if (strcmp(argv[i], "--output") == 0 && i+1 < argc)
strncpy(g_output_path, argv[++i], MAX_PATH_LEN-1);
else if (strcmp(argv[i], "--format") == 0 && i+1 < argc) {
if (strcmp(argv[i+1], "text") == 0) g_format_text = 1;
i++;
}
else if (strcmp(argv[i], "--filter") == 0 && i+1 < argc)
strncpy(g_filter_undf, argv[++i], 63);
else if (strcmp(argv[i], "--verbose") == 0)
g_verbose = 1;
else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
usage(argv[0]);
return 0;
} else {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
usage(argv[0]);
return 1;
}
}
/* Determine thread count */
if (g_threads <= 0) {
long nproc = sysconf(_SC_NPROCESSORS_ONLN);
g_threads = (nproc > 0) ? (int)nproc : 4;
}
if (g_threads > 64) g_threads = 64;
fprintf(stderr, "undfscand v1.0.0 — target=%s threads=%d\n",
g_target_dir, g_threads);
/* Parse registry */
if (parse_registry(g_registry_path) < 0) return 1;
if (g_n_defects == 0) {
fprintf(stderr, "ERROR: no defect entries parsed from registry\n");
return 1;
}
fprintf(stderr, "Registry: %d defects\n", g_n_defects);
/* Initialize result slots */
for (int i = 0; i < g_n_defects; i++) {
memset(&g_results[i], 0, sizeof(g_results[i]));
strncpy(g_results[i].undf_id, g_defects[i].undf_id, 63);
strncpy(g_results[i].defect_id, g_defects[i].defect_id, 127);
g_results[i].status = STATUS_UNKNOWN;
}
/* Enqueue jobs */
for (int i = 0; i < g_n_defects; i++) {
/* Apply filter if set */
if (g_filter_undf[0] && strcmp(g_defects[i].undf_id, g_filter_undf) != 0)
continue;
queue_push(i);
}
queue_signal_done();
/* Launch worker threads */
pthread_t *threads = malloc(g_threads * sizeof(pthread_t));
if (!threads) { perror("malloc"); return 1; }
for (int i = 0; i < g_threads; i++) {
if (pthread_create(&threads[i], NULL, worker_thread, NULL) != 0) {
perror("pthread_create"); return 1;
}
}
/* Wait for all workers */
for (int i = 0; i < g_threads; i++)
pthread_join(threads[i], NULL);
free(threads);
/* Write report */
FILE *out = stdout;
if (g_output_path[0]) {
out = fopen(g_output_path, "w");
if (!out) { perror("fopen output"); return 1; }
}
write_report(out);
if (g_output_path[0]) fclose(out);
/* Summary to stderr */
int unpatched = 0;
for (int i = 0; i < g_n_defects; i++)
if (g_results[i].status == STATUS_UNPATCHED) unpatched++;
fprintf(stderr, "Scan complete.\n");
if (g_output_path[0])
fprintf(stderr, "Report written to: %s\n", g_output_path);
return (unpatched > 0) ? 2 : 0;
}