/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* ============================================================ * 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+63state,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(lenbuf+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+63count+=512; } if(ibuf,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 | N–M | 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 | N–M | 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 N–M 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//-*.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; }