├── go.mod ├── internal ├── .DS_Store ├── ai │ ├── .DS_Store │ ├── azureai │ │ ├── .DS_Store │ │ └── azureai.go │ └── openai │ │ └── openai.go ├── markdown │ └── markdown.go └── scanner │ └── scanner.go ├── config.json ├── scanner.go ├── config ├── ascii.go └── config.go ├── README.md └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module source-code-review 2 | 3 | go 1.23.2 4 | -------------------------------------------------------------------------------- /internal/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecFathy/Flash/HEAD/internal/.DS_Store -------------------------------------------------------------------------------- /internal/ai/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecFathy/Flash/HEAD/internal/ai/.DS_Store -------------------------------------------------------------------------------- /internal/ai/azureai/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecFathy/Flash/HEAD/internal/ai/azureai/.DS_Store -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "azure_openai": { 3 | "endpoint": "", 4 | "api_key": "", 5 | "deployment_name": "", 6 | "api_version": "" 7 | }, 8 | "openai": { 9 | "api_key": "your-openai-api-key" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /internal/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func SaveMarkdown(fileName string, content string) error { 9 | filePath := fmt.Sprintf("%s.md", fileName) 10 | return os.WriteFile(filePath, []byte(content), 0644) 11 | } 12 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func ScanFile(filePath string) (string, error) { 9 | content, err := os.ReadFile(filePath) 10 | if err != nil { 11 | return "", err 12 | } 13 | 14 | return string(content), nil 15 | } 16 | 17 | func ScanDirectory(dirPath string) ([]string, error) { 18 | var files []string 19 | 20 | err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { 21 | if !info.IsDir() && filepath.Ext(path) == ".go" { 22 | files = append(files, path) 23 | } 24 | return nil 25 | }) 26 | 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return files, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/scanner/scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func ScanFile(filePath string) (string, error) { 9 | content, err := os.ReadFile(filePath) 10 | if err != nil { 11 | return "", err 12 | } 13 | 14 | return string(content), nil 15 | } 16 | 17 | func ScanDirectory(dirPath string) ([]string, error) { 18 | var files []string 19 | 20 | err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { 21 | if !info.IsDir() && filepath.Ext(path) == ".go" { 22 | files = append(files, path) 23 | } 24 | return nil 25 | }) 26 | 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return files, nil 32 | } 33 | -------------------------------------------------------------------------------- /config/ascii.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | const ( 6 | Yellow = "\033[33m" // Yellow color for normal information 7 | Red = "\033[31m" // Red color for warnings and errors 8 | Reset = "\033[0m" // Reset the color 9 | ) 10 | 11 | func Info(message string) { 12 | fmt.Printf("%s[INF] %s%s\n", Yellow, message, Reset) 13 | } 14 | func Warn(message string) { 15 | fmt.Printf("%s[WRN] %s%s\n", Red, message, Reset) 16 | } 17 | func ShowASCII() { 18 | asciiArt := ` 19 | _____ __ _____ _____ _____ 20 | | __| | | _ | __| | | 21 | | __| |__| |__ | | 22 | |__| |_____|__|__|_____|__|__| v1.0.1 23 | 24 | By Mohammed Fathy @Secfathy` 25 | fmt.Println(asciiArt) 26 | fmt.Println("") 27 | } 28 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // Config structure to hold all credentials 10 | type Config struct { 11 | AzureOpenAI struct { 12 | Endpoint string `json:"endpoint"` 13 | APIKey string `json:"api_key"` 14 | DeploymentName string `json:"deployment_name"` 15 | APIVersion string `json:"api_version"` 16 | } `json:"azure_openai"` 17 | OpenAI struct { 18 | APIKey string `json:"api_key"` 19 | } `json:"openai"` 20 | } 21 | 22 | // LoadConfig loads the configuration from a JSON file 23 | func LoadConfig(configFilePath string) (*Config, error) { 24 | file, err := os.Open(configFilePath) 25 | if err != nil { 26 | return nil, fmt.Errorf("error opening config file: %v", err) 27 | } 28 | defer file.Close() 29 | 30 | var config Config 31 | decoder := json.NewDecoder(file) 32 | err = decoder.Decode(&config) 33 | if err != nil { 34 | return nil, fmt.Errorf("error decoding config file: %v", err) 35 | } 36 | 37 | return &config, nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/ai/openai/openai.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type Client struct { 13 | APIKey string 14 | HTTPClient *http.Client 15 | } 16 | 17 | type Message struct { 18 | Role string `json:"role"` 19 | Content string `json:"content"` 20 | } 21 | 22 | type ChatRequest struct { 23 | Model string `json:"model"` 24 | Messages []Message `json:"messages"` 25 | MaxTokens int `json:"max_tokens"` 26 | Temperature float64 `json:"temperature"` 27 | } 28 | 29 | type ChatResponse struct { 30 | Choices []struct { 31 | Message struct { 32 | Content string `json:"content"` 33 | } `json:"message"` 34 | } `json:"choices"` 35 | } 36 | 37 | // NewClient initializes a new OpenAI ChatGPT client. 38 | func NewClient(apiKey string) (*Client, error) { 39 | if apiKey == "" { 40 | return nil, fmt.Errorf("API key must be provided") 41 | } 42 | 43 | httpClient := &http.Client{ 44 | Timeout: 30 * time.Second, 45 | } 46 | 47 | return &Client{ 48 | APIKey: apiKey, 49 | HTTPClient: httpClient, 50 | }, nil 51 | } 52 | 53 | // Chat sends a request to the OpenAI API. 54 | func (c *Client) Chat(messages []Message, model string, maxTokens int, temperature float64) (string, error) { 55 | url := "https://api.openai.com/v1/chat/completions" 56 | 57 | requestBody := ChatRequest{ 58 | Model: model, 59 | Messages: messages, 60 | MaxTokens: maxTokens, 61 | Temperature: temperature, 62 | } 63 | 64 | jsonBody, err := json.Marshal(requestBody) 65 | if err != nil { 66 | return "", fmt.Errorf("error marshaling request: %v", err) 67 | } 68 | 69 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) 70 | if err != nil { 71 | return "", fmt.Errorf("error creating request: %v", err) 72 | } 73 | 74 | req.Header.Set("Content-Type", "application/json") 75 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey)) 76 | 77 | resp, err := c.HTTPClient.Do(req) 78 | if err != nil { 79 | return "", fmt.Errorf("error sending request: %v", err) 80 | } 81 | defer resp.Body.Close() 82 | 83 | body, err := io.ReadAll(resp.Body) 84 | if err != nil { 85 | return "", fmt.Errorf("error reading response: %v", err) 86 | } 87 | 88 | if resp.StatusCode != http.StatusOK { 89 | return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) 90 | } 91 | 92 | var chatResponse ChatResponse 93 | if err := json.Unmarshal(body, &chatResponse); err != nil { 94 | return "", fmt.Errorf("error unmarshaling response: %v", err) 95 | } 96 | 97 | if len(chatResponse.Choices) == 0 { 98 | return "", fmt.Errorf("no choices in response") 99 | } 100 | 101 | return chatResponse.Choices[0].Message.Content, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/ai/azureai/azureai.go: -------------------------------------------------------------------------------- 1 | package azureai 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type Client struct { 13 | Endpoint string 14 | APIKey string 15 | DeploymentName string 16 | APIVersion string 17 | HTTPClient *http.Client 18 | } 19 | 20 | type Message struct { 21 | Role string `json:"role"` 22 | Content string `json:"content"` 23 | } 24 | 25 | type ChatRequest struct { 26 | Messages []Message `json:"messages"` 27 | MaxTokens int `json:"max_tokens"` 28 | Temperature float64 `json:"temperature"` 29 | } 30 | 31 | type ChatResponse struct { 32 | Choices []struct { 33 | Message struct { 34 | Content string `json:"content"` 35 | } `json:"message"` 36 | } `json:"choices"` 37 | } 38 | 39 | // NewClient initializes a new Azure AI client. 40 | func NewClient(endpoint, apiKey, deploymentName, apiVersion string) (*Client, error) { 41 | if endpoint == "" || apiKey == "" || deploymentName == "" || apiVersion == "" { 42 | return nil, fmt.Errorf("all parameters must be provided") 43 | } 44 | 45 | httpClient := &http.Client{ 46 | Timeout: 30 * time.Second, 47 | } 48 | 49 | return &Client{ 50 | Endpoint: endpoint, 51 | APIKey: apiKey, 52 | DeploymentName: deploymentName, 53 | APIVersion: apiVersion, 54 | HTTPClient: httpClient, 55 | }, nil 56 | } 57 | 58 | // Chat sends a request to Azure OpenAI. 59 | func (c *Client) Chat(messages []Message, maxTokens int, temperature float64) (string, error) { 60 | url := fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s", c.Endpoint, c.DeploymentName, c.APIVersion) 61 | 62 | requestBody := ChatRequest{ 63 | Messages: messages, 64 | MaxTokens: maxTokens, 65 | Temperature: temperature, 66 | } 67 | 68 | jsonBody, err := json.Marshal(requestBody) 69 | if err != nil { 70 | return "", fmt.Errorf("error marshaling request: %v", err) 71 | } 72 | 73 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) 74 | if err != nil { 75 | return "", fmt.Errorf("error creating request: %v", err) 76 | } 77 | 78 | req.Header.Set("Content-Type", "application/json") 79 | req.Header.Set("api-key", c.APIKey) 80 | 81 | resp, err := c.HTTPClient.Do(req) 82 | if err != nil { 83 | return "", fmt.Errorf("error sending request: %v", err) 84 | } 85 | defer resp.Body.Close() 86 | 87 | body, err := io.ReadAll(resp.Body) 88 | if err != nil { 89 | return "", fmt.Errorf("error reading response: %v", err) 90 | } 91 | 92 | if resp.StatusCode != http.StatusOK { 93 | return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) 94 | } 95 | 96 | var chatResponse ChatResponse 97 | if err := json.Unmarshal(body, &chatResponse); err != nil { 98 | return "", fmt.Errorf("error unmarshaling response: %v", err) 99 | } 100 | 101 | if len(chatResponse.Choices) == 0 { 102 | return "", fmt.Errorf("no choices in response") 103 | } 104 | 105 | return chatResponse.Choices[0].Message.Content, nil 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Flash 4 | 5 | [![Go](https://img.shields.io/badge/Go-1.19-blue.svg?style=flat&logo=go)](https://golang.org) 6 | [![Open Source](https://img.shields.io/badge/Open%20Source-%F0%9F%92%9A-brightgreen?style=flat)](https://opensource.org) 7 | [![Azure](https://img.shields.io/badge/Azure-Cloud-blue.svg?style=flat&logo=microsoft-azure)](https://azure.microsoft.com/) 8 | [![Powered by ChatGPT](https://img.shields.io/badge/Powered%20by-ChatGPT-ff69b4.svg?style=flat&logo=openai)](https://openai.com/) 9 | 10 |
11 | 12 | **Flash** is an AI-powered code vulnerability scanner designed to help developers identify security vulnerabilities in their code. By leveraging AI models like OpenAI and Azure OpenAI, Flash automates the review process for various coding languages and provides detailed reports with potential vulnerabilities, proof of concepts, and recommended fixes. Flash can generate reports in Markdown format, making it easy for developers to integrate security analysis into their workflow. 13 | 14 | ## Features 15 | 16 | - **AI-Powered Code Analysis**: Leverages OpenAI's GPT models to analyze code and detect potential security vulnerabilities. 17 | - **Multi-Platform Support**: Flash works across various platforms and languages, making it a flexible solution for code review. 18 | - **Detailed Vulnerability Reports**: Generates reports with detailed descriptions of identified vulnerabilities, proof of concepts, and recommended fixes. 19 | - **Supports Multiple Languages**: Works with PHP, Python, JavaScript, and more. 20 | - **Markdown Report Generation**: Outputs security analysis in Markdown format for easy integration with GitHub and other platforms. 21 | 22 | ## Installation 23 | 24 | 1. Clone the repository: 25 | ```bash 26 | git clone https://github.com/secfathy/flash.git 27 | ``` 28 | 29 | 2. Navigate to the project directory: 30 | ```bash 31 | cd flash 32 | ``` 33 | 34 | 3. Build the application: 35 | ```bash 36 | go build 37 | ``` 38 | 39 | 4. Run the application: 40 | ```bash 41 | go run main.go -file -save -config config.json 42 | ``` 43 | 44 | ## Usage 45 | 46 | Flash scans code files for vulnerabilities by sending code snippets to AI models, which then return a detailed analysis of the vulnerabilities. The results can be saved as Markdown reports. 47 | 48 | ### Command-line Options: 49 | 50 | - `-file`: Path to the code file to be analyzed. 51 | - `-dir`: Path to the directory of files to be analyzed. 52 | - `-save`: Directory to save the results (default is current directory). 53 | - `-config`: Path to the configuration file (default is `config.json`). 54 | - `-use-azure`: Set to `true` if using Azure OpenAI, `false` otherwise. 55 | 56 | ### Example: 57 | 58 | ```bash 59 | go run main.go -file example.php -save /home/reports -config config.json -use-azure=true 60 | 61 | ``` 62 | 63 | ### Configuration File (config.json) 64 | 65 | The `config.json` file contains API keys and endpoint information for both OpenAI and Azure OpenAI. You can configure it as shown below: 66 | 67 | ```json 68 | { 69 | "azure_openai": { 70 | "endpoint": "your-azure-openai-endpoint", 71 | "api_key": "your-azure-api-key", 72 | "deployment_name": "gpt-4o", 73 | "api_version": "2024-02-15-preview" 74 | }, 75 | "openai": { 76 | "api_key": "your-openai-api-key" 77 | } 78 | } 79 | 80 | 81 | ``` 82 | 83 | 84 | ## Contribution 85 | 86 | Contributions are welcome! To contribute: 87 | 88 | 1. Fork the repository. 89 | 2. Create a new branch: `git checkout -b feature-branch-name`. 90 | 3. Make your changes and commit them: `git commit -m 'Add new feature'`. 91 | 4. Push to the branch: `git push origin feature-branch-name`. 92 | 5. Open a pull request. 93 | 94 | ## License 95 | 96 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 97 | 98 | ## Contact 99 | 100 | For questions or support, feel free to reach out to the repository owner. 101 | 102 | --- 103 | 104 | Developed with ❤️ by [Mohammed Fathy @Secfathy](https://github.com/Secfathy) 105 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "syscall" 10 | 11 | "source-code-review/config" // Import config package for ASCII art and colored logging 12 | "source-code-review/internal/ai/azureai" // Import Azure OpenAI client 13 | "source-code-review/internal/ai/openai" // Import OpenAI ChatGPT client 14 | "source-code-review/internal/markdown" 15 | "source-code-review/internal/scanner" 16 | ) 17 | 18 | func main() { 19 | // Show ASCII art at startup 20 | config.ShowASCII() 21 | 22 | // Handle graceful shutdown signals (SIGINT, SIGTERM) 23 | // This channel listens for system signals 24 | sigs := make(chan os.Signal, 1) 25 | // Notify the channel if an interrupt or terminate signal is received 26 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 27 | 28 | // Command line arguments for file or directory and whether to use Azure OpenAI or OpenAI 29 | filePtr := flag.String("file", "", "File to scan for vulnerabilities") 30 | dirPtr := flag.String("dir", "", "Directory to scan for vulnerabilities") 31 | useAzure := flag.Bool("use-azure", false, "Use Azure OpenAI if true, otherwise use OpenAI") 32 | configPath := flag.String("config", "config.json", "Path to the configuration file") 33 | saveDir := flag.String("save", ".", "Directory to save the results (default is current directory)") // Custom save directory 34 | flag.Parse() 35 | 36 | // Check if the save directory exists 37 | if _, err := os.Stat(*saveDir); os.IsNotExist(err) { 38 | config.Warn(fmt.Sprintf("Save directory does not exist: %s", *saveDir)) 39 | os.Exit(1) 40 | } 41 | 42 | // Load configuration from file 43 | cfg, err := config.LoadConfig(*configPath) 44 | if err != nil { 45 | config.Warn(fmt.Sprintf("Failed to load config: %v", err)) 46 | os.Exit(1) 47 | } 48 | 49 | // Signal handling routine in a separate goroutine 50 | go func() { 51 | // Wait for the signal 52 | sig := <-sigs 53 | // When a signal is received, attempt graceful shutdown 54 | config.Info(fmt.Sprintf("Received signal: %s. Attempting graceful shutdown.", sig)) 55 | os.Exit(0) 56 | }() 57 | 58 | var filesToScan []string 59 | if *filePtr != "" { 60 | filesToScan = append(filesToScan, *filePtr) 61 | } else if *dirPtr != "" { 62 | files, err := scanner.ScanDirectory(*dirPtr) 63 | if err != nil { 64 | config.Warn(fmt.Sprintf("Failed to scan directory: %v", err)) 65 | os.Exit(1) 66 | } 67 | filesToScan = files 68 | } else { 69 | config.Warn("No file or directory specified") 70 | os.Exit(1) 71 | } 72 | 73 | // Iterate over the files and process each one 74 | for _, file := range filesToScan { 75 | config.Info(fmt.Sprintf("Scanning %s with Azure OpenAI", file)) 76 | 77 | // Read the file content 78 | content, err := scanner.ScanFile(file) 79 | if err != nil { 80 | config.Warn(fmt.Sprintf("Failed to read file: %v", err)) 81 | os.Exit(1) 82 | } 83 | 84 | var result string 85 | 86 | // Determine whether to use Azure OpenAI or OpenAI ChatGPT 87 | if *useAzure { 88 | // Initialize Azure OpenAI client from configuration 89 | client, err := azureai.NewClient(cfg.AzureOpenAI.Endpoint, cfg.AzureOpenAI.APIKey, cfg.AzureOpenAI.DeploymentName, cfg.AzureOpenAI.APIVersion) 90 | if err != nil { 91 | config.Warn(fmt.Sprintf("Failed to create Azure AI client: %v", err)) 92 | os.Exit(1) 93 | } 94 | 95 | messages := []azureai.Message{ 96 | {Role: "user", Content: content}, 97 | } 98 | result, err = client.Chat(messages, 800, 0.7) 99 | if err != nil { 100 | config.Warn(fmt.Sprintf("Failed to get AI response: %v", err)) 101 | os.Exit(1) 102 | } 103 | 104 | } else { 105 | // Initialize OpenAI client from configuration 106 | client, err := openai.NewClient(cfg.OpenAI.APIKey) 107 | if err != nil { 108 | config.Warn(fmt.Sprintf("Failed to create OpenAI client: %v", err)) 109 | os.Exit(1) 110 | } 111 | 112 | messages := []openai.Message{ 113 | {Role: "user", Content: content}, 114 | } 115 | result, err = client.Chat(messages, "gpt-3.5-turbo", 800, 0.7) 116 | if err != nil { 117 | config.Warn(fmt.Sprintf("Failed to get AI response: %v", err)) 118 | os.Exit(1) 119 | } 120 | } 121 | 122 | // Save the result to a markdown file in the specified save directory 123 | config.Info(fmt.Sprintf("Saving results for %s", file)) 124 | baseName := filepath.Base(file) // Get the base file name (without directories) 125 | resultFile := filepath.Join(*saveDir, baseName+".md") // Save the result in the custom directory 126 | err = markdown.SaveMarkdown(resultFile, result) 127 | if err != nil { 128 | config.Warn(fmt.Sprintf("Failed to save markdown for %s: %v", file, err)) 129 | os.Exit(1) 130 | } 131 | } 132 | 133 | config.Info("Scan complete.") 134 | } 135 | --------------------------------------------------------------------------------