import datetime
import os
import gc
import warnings
import json
import glob
import shutil
import pandas as pd
from tqdm import tqdm
import multiprocessing as mp
from collections import Counter
from util import *
import ast
from nltk import word_tokenize
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import requests
from bs4 import BeautifulSoup
import re
import logging
import time
from collections import OrderedDict
import nltk.data
from nltk import word_tokenize
from nltk.corpus import stopwords
cache_dir = os.path.abspath('../transformers_cache')
os.makedirs(cache_dir, exist_ok=True)
os.environ['TRANSFORMERS_CACHE'] = cache_dir
os.environ['HF_HOME'] = cache_dir
print(f"Transformers cache directory set to: {cache_dir}")
import numpy as np
import pandas as pd
from tqdm import tqdm
import multiprocessing as mp
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from transformers import AutoTokenizer
from transformers import (AdamW, get_linear_schedule_with_warmup,
RobertaConfig, RobertaTokenizer, RobertaModel, AutoModel)
from transformers import AutoModelForSeq2SeqLM, AdamW, get_linear_schedule_with_warmup
if torch.cuda.is_available():
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
device = torch.device("cuda")
print("Using CUDA device")
else:
device = torch.device("cpu")
print("Using CPU device")
tqdm.pandas()
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
stop_words = set(stopwords.words('english'))
stemmer = nltk.stem.SnowballStemmer('english')
stop_words.add('.')
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
def RemoveGit(str):
gitPattern = '[Gg]it-svn-id'
return re.sub(gitPattern, ' ', str)
def clean_en_text(text):
text = text.lower()
text = re.sub(r'[^a-zA-Z0-9 ]', ' ', text)
text = ' '.join(text.split())
return text
def textProcess(text):
final = []
text = RemoveGit(text)
sentences = tokenizer.tokenize(text)
for sentence in sentences:
sentence = clean_en_text(sentence)
word_tokens = word_tokenize(sentence)
word_tokens = [word for word in word_tokens if word not in stop_words]
for word in word_tokens:
final.append(str(stemmer.stem(word)))
if len(final) == 0:
text = ' '
else:
text = ' '.join(final)
return text
def diffcodeProcess(diffcode):
added_code = ''
deleted_code = ''
added_annotation = ''
deleted_annotation = ''
lines = diffcode.split('\n')
for line in lines:
if line.startswith('+') and not line.startswith('++'):
line = line[1:].strip()
added_code = added_code + line + ' '
if line.startswith('-') and not line.startswith('--'):
line = line[1:].strip()
deleted_code = deleted_code + line + ' '
return added_code, deleted_code, added_annotation, deleted_annotation
def convert_examples_to_features(desc, mess, tokenizer, max_seq_length):
desc_token = tokenizer.tokenize(desc)
mess_token = tokenizer.tokenize(mess)
if len(desc_token) + len(mess_token) > max_seq_length - 3:
if len(desc_token) > (max_seq_length - 3) / 2 and len(mess_token) > (max_seq_length - 3) / 2:
desc_token = desc_token[:int((max_seq_length - 3) / 2)]
mess_token = mess_token[:max_seq_length - 3 - len(desc_token)]
elif len(desc_token) > (max_seq_length - 3) / 2:
desc_token = desc_token[:max_seq_length - 3 - len(mess_token)]
elif len(mess_token) > (max_seq_length - 3) / 2:
mess_token = mess_token[:max_seq_length - 3 - len(desc_token)]
combined_token = [tokenizer.cls_token] + desc_token + [tokenizer.sep_token] + mess_token + [tokenizer.sep_token]
input_ids_text = tokenizer.convert_tokens_to_ids(combined_token)
if len(input_ids_text) < max_seq_length:
padding_length = max_seq_length - len(input_ids_text)
input_ids_text += [tokenizer.pad_token_id] * padding_length
input_ids_text = torch.tensor(input_ids_text)
assert len(input_ids_text) == max_seq_length, 'Length of input_ids_text is error!'
attention_mask_text = input_ids_text.ne(tokenizer.pad_token_id).to(torch.int64)
return input_ids_text, attention_mask_text
def get_rank(df1, sortby, ascending=False):
gb = df1.groupby('cve')
l = []
for item1, item2 in gb:
item2 = item2.reset_index()
item2 = item2.sort_values(sortby + ['commit'], ascending=ascending)
item2 = item2.reset_index(drop=True).reset_index()
l.append(item2[['index', 'level_0']])
df1 = pd.concat(l)
df1['rank'] = df1['level_0'] + 1
df1 = df1.sort_values(['index'], ascending=True).reset_index(drop=True)
return df1['rank']
def get_metrics_N(test, rankname='rank', N=10):
cve_list = []
cnt = 0
total = []
gb = test.groupby('cve')
for item1, item2 in gb:
item2 = item2.sort_values([rankname], ascending=True).reset_index(drop=True)
idx = item2[item2.label == 1].index[-1] + 1
if idx <= N:
total.append(idx)
cnt += 1
else:
total.append(N)
cve_list.append(item1)
return cnt / len(total), np.mean(total)
class SimpleDataset(Dataset):
def __init__(self, cve, feature_file_path, cve_file_path):
self.df = pd.read_csv(feature_file_path)
df_cve = pd.read_csv(cve_file_path)
df_cve = df_cve[['cve', 'desc']]
self.df = self.df.merge(df_cve, how='left', on='cve')
self.df = self.df.fillna('')
print("Processing CVE descriptions...")
with mp.Pool(mp.cpu_count()) as pool:
self.df['desc'] = list(tqdm(pool.imap(textProcess, self.df['desc']), total=len(self.df),
desc='Processing cve_desc'))
self.desc = self.df['desc']
print("Processing commit messages...")
with mp.Pool(mp.cpu_count()) as pool:
self.df['msg_text'] = list(tqdm(pool.imap(textProcess, self.df['msg_text']), total=len(self.df),
desc='Processing msg_text'))
self.mess = self.df['msg_text']
print("Processing diff code...")
with mp.Pool(mp.cpu_count()) as pool:
results = list(tqdm(pool.imap(diffcodeProcess, self.df['diff_code']), total=len(self.df),
desc='Processing diff_code'))
self.df['added_code'], self.df['deleted_code'], self.df['added_an'], self.df['deleted_an'] = zip(*results)
self.added_code = self.df['added_code']
self.deleted_code = self.df['deleted_code']
self.cve = self.df['cve']
self.commit = self.df['commit']
self.df['label'] = 0
if 'label' not in self.df.columns:
self.df['label'] = 0
self.label = self.df['label']
handcrafted_columns = ['addcnt', 'delcnt', 'totalcnt', 'issue_cnt', 'web_cnt', 'bug_cnt', 'cve_cnt',
'cve_match', 'bug_match', 'issue_match', 'cwe_match',
'time_dis', 'vuln_type_1', 'vuln_type_2', 'vuln_type_3',
'filepath_same_cnt', 'filepath_same_ratio', 'filepath_unrelated_cnt',
'file_same_cnt', 'file_same_ratio', 'file_unrelated_cnt',
'func_same_cnt', 'func_same_ratio', 'func_unrelated_cnt',
'inter_token_cwe_cnt', 'inter_token_cwe_ratio', 'commit_vuln_tfidf',
'mess_shared_num', 'mess_shared_ratio', 'mess_max', 'mess_sum', 'mess_mean', 'mess_var',
'code_shared_num', 'code_shared_ratio', 'code_max', 'code_sum', 'code_mean', 'code_var']
missing_columns = [col for col in handcrafted_columns if col not in self.df.columns]
if missing_columns:
print(f"Warning: Missing columns: {missing_columns}")
for col in missing_columns:
self.df[col] = 0
handcrafted_feature = self.df[handcrafted_columns]
self.handcrafted = handcrafted_feature.to_numpy()
roberta_model_path = '../pretrained_model/roberta-large'
codereviewer_model_path = '../pretrained_model/codereviewer'
if not os.path.exists(roberta_model_path):
print(f"Local RoBERTa model not found at {roberta_model_path}, downloading from Hugging Face...")
roberta_model_path = 'roberta-large'
if not os.path.exists(codereviewer_model_path):
print(f"Local CodeReviewer model not found at {codereviewer_model_path}, downloading from Hugging Face...")
codereviewer_model_path = 'microsoft/codereviewer'
self.text_tokenizer = RobertaTokenizer.from_pretrained(roberta_model_path, cache_dir=cache_dir)
self.code_tokenizer = AutoTokenizer.from_pretrained(codereviewer_model_path, cache_dir=cache_dir)
gc.collect()
def __len__(self):
return len(self.cve)
def __getitem__(self, index):
max_seq_length = 512
desc = self.desc[index] if isinstance(self.desc[index], str) else ''
mess = self.mess[index] if isinstance(self.mess[index], str) else ''
input_ids_text, attention_mask_text = convert_examples_to_features(desc, mess, self.text_tokenizer,
max_seq_length)
added_code = self.added_code[index] if isinstance(self.added_code[index], str) else ''
deleted_code = self.deleted_code[index] if isinstance(self.deleted_code[index], str) else ''
input_ids_diff, attention_mask_diff = convert_examples_to_features(added_code, deleted_code,
self.code_tokenizer, max_seq_length)
sample = (input_ids_text, attention_mask_text, input_ids_diff, attention_mask_diff,
torch.tensor(self.handcrafted[index]), torch.tensor(0),
self.cve[index], self.commit[index])
return sample
class NewModel(nn.Module):
def __init__(self):
super(NewModel, self).__init__()
self.hc_dim = 39
self.s_dim = 32
roberta_model_path = '../pretrained_model/roberta-large'
codereviewer_model_path = '../pretrained_model/codereviewer'
if not os.path.exists(roberta_model_path):
print(f"Local RoBERTa model not found at {roberta_model_path}, downloading from Hugging Face...")
roberta_model_path = 'roberta-large'
if not os.path.exists(codereviewer_model_path):
print(f"Local CodeReviewer model not found at {codereviewer_model_path}, downloading from Hugging Face...")
codereviewer_model_path = 'microsoft/codereviewer'
config = RobertaConfig.from_pretrained(roberta_model_path, cache_dir=cache_dir)
config.num_labels = 1
self.textEncoder = AutoModel.from_pretrained(roberta_model_path, config=config, cache_dir=cache_dir)
self.codeEncoder = AutoModelForSeq2SeqLM.from_pretrained(codereviewer_model_path, cache_dir=cache_dir).encoder
self.fc1 = nn.Linear(self.textEncoder.config.hidden_size, self.s_dim)
self.fc2 = nn.Linear(self.codeEncoder.config.hidden_size, self.s_dim)
self.mlp = nn.Sequential(
nn.Linear(self.s_dim * 2 + self.hc_dim, (self.s_dim * 2 + self.hc_dim) // 2),
nn.ReLU(),
nn.Linear((self.s_dim * 2 + self.hc_dim) // 2, 2)
)
self.criterion = nn.CrossEntropyLoss()
for param in self.textEncoder.parameters():
param.requires_grad = True
for param in self.codeEncoder.parameters():
param.requires_grad = True
def forward(self, input_ids_text, attention_mask_text, input_ids_diff, attention_mask_diff, handcrafted, label=None):
text_output = self.textEncoder(input_ids=input_ids_text, attention_mask=attention_mask_text)[1]
code_output = self.codeEncoder(input_ids=input_ids_diff, attention_mask=attention_mask_diff).last_hidden_state[:, 0, :]
text_output = self.fc1(text_output)
code_output = self.fc2(code_output)
combine_output = torch.cat([text_output, code_output], dim=-1)
combine_output = torch.cat([combine_output, handcrafted], dim=-1)
logits = self.mlp(combine_output)
prob = torch.softmax(logits, -1)
if label is not None:
loss = self.criterion(logits, label)
return loss, prob
else:
return prob
def test_single_cve(model, test_dataloader, result_dir, cve_name):
prob_list = []
label_list = []
cve_list = []
commit_list = []
model.eval()
bar = tqdm(test_dataloader, total=len(test_dataloader))
for step, batch in enumerate(bar):
input_ids_text = batch[0].to(device)
attention_mask_text = batch[1].to(device)
input_ids_diff = batch[2].to(device)
attention_mask_diff = batch[3].to(device)
handcrafted = batch[4].float().to(device)
label = batch[5].long().to(device)
cve = batch[6]
commit = batch[7]
with torch.no_grad():
prob = model(input_ids_text, attention_mask_text, input_ids_diff, attention_mask_diff, handcrafted)
prob_list.append(prob.cpu().numpy())
label_list.append(label.cpu().numpy())
cve_list.append(list(cve))
commit_list.append(list(commit))
cve_list = np.concatenate(cve_list, 0)
prob_list = np.concatenate(prob_list, 0)
prob_list = prob_list[:, 1]
label_list = np.concatenate(label_list, 0)
commit_list = np.concatenate(commit_list, 0)
p_data = {
'cve': cve_list,
'commit': commit_list,
'predict': prob_list
}
result_csv = pd.DataFrame(p_data)
result_csv['rank'] = get_rank(result_csv, ['predict'], ascending=False)
result_csv.to_csv(f'{result_dir}/rank_result_{cve_name}.csv', index=False)
print(f"Results for {cve_name}:")
print(f"Total commits: {len(result_csv)}")
print(f"Predictions saved to: {result_dir}/rank_result_{cve_name}.csv")
top_results = result_csv.sort_values('predict', ascending=False).head(10)
print("\nTop 10 predictions:")
print(top_results[['commit', 'predict', 'rank']].to_string(index=False))
return result_csv
def initial_ranking(cve_name):
model_path = "../checkpoint_Phase1_full_model.bin"
feature_file_path = f"../cache/{cve_name}_feature.csv"
cve_file_path = f"../cache/{cve_name}.csv"
result_dir = "../cache"
commit_file_path = f"../cache/{cve_name}_commits.csv"
print(f'Start testing for CVE: {cve_name} at', time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
if not os.path.exists(result_dir):
os.makedirs(result_dir)
print('1/3: Preparing dataset...')
test_data = SimpleDataset(cve_name, feature_file_path, cve_file_path)
test_dataloader = DataLoader(dataset=test_data, batch_size=32, num_workers=0)
print('2/3: Loading model...')
model = NewModel()
if not os.path.exists(model_path):
print(f"Warning: Model file not found at {model_path}")
print("Creating dummy model for testing...")
checkpoint = None
else:
checkpoint = torch.load(model_path, map_location=device)
if checkpoint is not None:
if isinstance(checkpoint, torch.nn.DataParallel):
state_dict = checkpoint.module.state_dict()
elif isinstance(checkpoint, dict) and 'module.' in list(checkpoint.keys())[0]:
state_dict = {k.replace('module.', ''): v for k, v in checkpoint.items()}
else:
state_dict = checkpoint
try:
model.load_state_dict(state_dict)
print("Model loaded successfully")
except Exception as e:
print(f"Warning: Could not load model weights: {e}")
print("Using randomly initialized model")
else:
print("Using randomly initialized model for testing")
model = model.to(device)
if device.type == 'cuda' and torch.cuda.device_count() > 1:
model = torch.nn.DataParallel(model)
print(f"Using DataParallel with {torch.cuda.device_count()} GPUs")
else:
print(f"Using single device: {device}")
print('3/3: Running test...')
result_csv = test_single_cve(model, test_dataloader, result_dir, cve_name)
print('*************** Test completed ***************')
top50 = result_csv.sort_values('predict', ascending=False).head(50)
top50.drop(columns=['predict', 'rank', 'cve'], inplace=True)
df1 = pd.read_csv(commit_file_path)
top50 = top50.merge(df1, how='left', on='commit')
df2 = pd.read_csv(cve_file_path)
df2.drop(columns=['repo'], inplace=True)
top50 = top50.merge(df2, how='left', on='cve')
top50['author_username'] = top50['author']
top50['committer_username'] = top50['committer']
top50 = top50[['cve', 'repo', 'commit', 'msg_text', 'msg_url', 'diff_code', 'commit_time', 'desc', 'cwe', 'author_username', 'committer_username']]
top50.to_csv(f'{result_dir}/{cve_name}_top50.csv', index=False)
return result_csv
if __name__ == "__main__":
cve_name = "CVE-2020-26249"
result_csv = initial_ranking(cve_name)