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.
1085 lines
41 KiB
C
1085 lines
41 KiB
C
/*
|
||
* 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 | 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/<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;
|
||
}
|