2023-06-21 18:49:40 +05:30
package main
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/levels"
2023-10-17 17:44:13 +05:30
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
2025-02-11 04:31:37 +07:00
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
2023-06-21 18:49:40 +05:30
"github.com/projectdiscovery/retryablehttp-go"
2025-08-20 05:28:23 +05:30
"github.com/projectdiscovery/utils/errkit"
2023-06-21 18:49:40 +05:30
"gopkg.in/yaml.v3"
)
const (
yamlIndentSpaces = 2
// templateman api base url
tmBaseUrlDefault = "https://tm.nuclei.sh"
)
var tmBaseUrl string
func init ( ) {
tmBaseUrl = os . Getenv ( "TEMPLATEMAN_SERVER" )
if tmBaseUrl == "" {
tmBaseUrl = tmBaseUrlDefault
}
}
// allTagsRegex is a list of all tags in nuclei templates except id, info, and -
var allTagsRegex [ ] * regexp . Regexp
var defaultOpts = types . DefaultOptions ( )
func init ( ) {
var tm templates . Template
t := reflect . TypeOf ( tm )
for i := 0 ; i < t . NumField ( ) ; i ++ {
tag := t . Field ( i ) . Tag . Get ( "yaml" )
if strings . Contains ( tag , "," ) {
tag = strings . Split ( tag , "," ) [ 0 ]
}
// ignore these tags
if tag == "id" || tag == "info" || tag == "" || tag == "-" {
continue
}
re := regexp . MustCompile ( tag + ` :\s*\n ` )
if t . Field ( i ) . Type . Kind ( ) == reflect . Bool {
re = regexp . MustCompile ( tag + ` :\s*(true|false)\s*\n ` )
}
allTagsRegex = append ( allTagsRegex , re )
}
// need to set headless to true for headless templates
defaultOpts . Headless = true
2023-11-16 17:56:07 +05:30
defaultOpts . EnableCodeTemplates = true
2024-11-19 19:30:28 +03:00
defaultOpts . EnableSelfContainedTemplates = true
2023-06-21 18:49:40 +05:30
if err := protocolstate . Init ( defaultOpts ) ; err != nil {
gologger . Fatal ( ) . Msgf ( "Could not initialize protocol state: %s\n" , err )
}
if err := protocolinit . Init ( defaultOpts ) ; err != nil {
gologger . Fatal ( ) . Msgf ( "Could not initialize protocol state: %s\n" , err )
}
}
type options struct {
input string
errorLogFile string
lint bool
validate bool
format bool
enhance bool
maxRequest bool
debug bool
}
func main ( ) {
opts := options { }
flagSet := goflags . NewFlagSet ( )
2023-08-01 14:33:43 -04:00
flagSet . SetDescription ( ` TemplateMan CLI is basic utility built on the TemplateMan API to standardize nuclei templates. ` )
2023-06-21 18:49:40 +05:30
flagSet . CreateGroup ( "Input" , "input" ,
flagSet . StringVarP ( & opts . input , "input" , "i" , "" , "Templates to annotate" ) ,
)
flagSet . CreateGroup ( "Config" , "config" ,
flagSet . BoolVarP ( & opts . lint , "lint" , "l" , false , "lint given nuclei template" ) ,
flagSet . BoolVarP ( & opts . validate , "validate" , "v" , false , "validate given nuclei template" ) ,
flagSet . BoolVarP ( & opts . format , "format" , "f" , false , "format given nuclei template" ) ,
flagSet . BoolVarP ( & opts . enhance , "enhance" , "e" , false , "enhance given nuclei template" ) ,
flagSet . BoolVarP ( & opts . maxRequest , "max-request" , "mr" , false , "add / update max request counter" ) ,
flagSet . StringVarP ( & opts . errorLogFile , "error-log" , "el" , "" , "file to write failed template update" ) ,
flagSet . BoolVarP ( & opts . debug , "debug" , "d" , false , "show debug message" ) ,
)
if err := flagSet . Parse ( ) ; err != nil {
gologger . Fatal ( ) . Msgf ( "Error parsing flags: %s\n" , err )
}
if opts . input == "" {
gologger . Fatal ( ) . Msg ( "input template path/directory is required" )
}
if strings . HasPrefix ( opts . input , "~/" ) {
home , err := os . UserHomeDir ( )
if err != nil {
log . Fatalf ( "Failed to read UserHomeDir: %v, provide absolute template path/directory\n" , err )
}
opts . input = filepath . Join ( home , ( opts . input ) [ 2 : ] )
}
gologger . DefaultLogger . SetMaxLevel ( levels . LevelInfo )
if opts . debug {
gologger . DefaultLogger . SetMaxLevel ( levels . LevelDebug )
}
if err := process ( opts ) ; err != nil {
gologger . Error ( ) . Msgf ( "could not process: %s\n" , err )
}
}
func process ( opts options ) error {
2025-04-21 16:18:14 +03:30
tempDir , err := os . MkdirTemp ( "" , "nuclei-nvd" )
2023-06-21 18:49:40 +05:30
if err != nil {
return err
}
2025-07-01 00:40:44 +07:00
defer func ( ) {
_ = os . RemoveAll ( tempDir )
} ( )
2023-06-21 18:49:40 +05:30
var errFile * os . File
if opts . errorLogFile != "" {
errFile , err = os . OpenFile ( opts . errorLogFile , os . O_CREATE | os . O_WRONLY | os . O_TRUNC , 0644 )
if err != nil {
gologger . Fatal ( ) . Msgf ( "could not open error log file: %s\n" , err )
}
2025-07-01 00:40:44 +07:00
defer func ( ) {
2025-07-09 14:47:26 -05:00
_ = errFile . Close ( )
} ( )
2023-06-21 18:49:40 +05:30
}
templateCatalog := disk . NewCatalog ( filepath . Dir ( opts . input ) )
paths , err := templateCatalog . GetTemplatePath ( opts . input )
if err != nil {
return err
}
for _ , path := range paths {
data , err := os . ReadFile ( path )
if err != nil {
return err
}
dataString := string ( data )
if opts . maxRequest {
var updated bool // if max-requests is updated
dataString , updated , err = parseAndAddMaxRequests ( templateCatalog , path , dataString )
if err != nil {
2024-08-16 08:01:23 -07:00
gologger . Info ( ) . Label ( "max-request" ) . Msg ( logErrMsg ( path , err , opts . debug , errFile ) )
2023-06-21 18:49:40 +05:30
} else {
if updated {
gologger . Info ( ) . Label ( "max-request" ) . Msgf ( "✅ updated template: %s\n" , path )
}
// do not print if max-requests is not updated
}
}
if opts . lint {
lint , err := lintTemplate ( dataString )
if err != nil {
gologger . Info ( ) . Label ( "lint" ) . Msg ( logErrMsg ( path , err , opts . debug , errFile ) )
}
if lint {
gologger . Info ( ) . Label ( "lint" ) . Msgf ( "✅ lint template: %s\n" , path )
}
}
if opts . validate {
validate , err := validateTemplate ( dataString )
if err != nil {
gologger . Info ( ) . Label ( "validate" ) . Msg ( logErrMsg ( path , err , opts . debug , errFile ) )
}
if validate {
gologger . Info ( ) . Label ( "validate" ) . Msgf ( "✅ validated template: %s\n" , path )
}
}
if opts . format {
formatedTemplateData , isFormated , err := formatTemplate ( dataString )
if err != nil {
gologger . Info ( ) . Label ( "format" ) . Msg ( logErrMsg ( path , err , opts . debug , errFile ) )
} else {
if isFormated {
_ = os . WriteFile ( path , [ ] byte ( formatedTemplateData ) , 0644 )
dataString = formatedTemplateData
gologger . Info ( ) . Label ( "format" ) . Msgf ( "✅ formated template: %s\n" , path )
}
}
}
if opts . enhance {
enhancedTemplateData , isEnhanced , err := enhanceTemplate ( dataString )
if err != nil {
gologger . Info ( ) . Label ( "enhance" ) . Msg ( logErrMsg ( path , err , opts . debug , errFile ) )
continue
} else {
if isEnhanced {
_ = os . WriteFile ( path , [ ] byte ( enhancedTemplateData ) , 0644 )
gologger . Info ( ) . Label ( "enhance" ) . Msgf ( "✅ updated template: %s\n" , path )
}
}
}
}
return nil
}
func logErrMsg ( path string , err error , debug bool , errFile * os . File ) string {
msg := fmt . Sprintf ( "❌ template: %s\n" , path )
if debug {
msg = fmt . Sprintf ( "❌ template: %s err: %s\n" , path , err )
}
if errFile != nil {
2025-07-01 00:40:44 +07:00
_ , _ = fmt . Fprintf ( errFile , "❌ template: %s err: %s\n" , path , err )
2023-06-21 18:49:40 +05:30
}
return msg
}
2024-04-07 00:03:24 +08:00
// enhanceTemplate enhances template data using templateman
2023-06-21 18:49:40 +05:30
// ref: https://github.com/projectdiscovery/templateman/blob/main/templateman-rest-api/README.md#enhance-api
func enhanceTemplate ( data string ) ( string , bool , error ) {
resp , err := retryablehttp . DefaultClient ( ) . Post ( fmt . Sprintf ( "%s/enhance" , tmBaseUrl ) , "application/x-yaml" , strings . NewReader ( data ) )
if err != nil {
return data , false , err
}
if resp . StatusCode != 200 {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "unexpected status code: %v" , resp . Status ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
var templateResp TemplateResp
if err := json . NewDecoder ( resp . Body ) . Decode ( & templateResp ) ; err != nil {
return data , false , err
}
if strings . TrimSpace ( templateResp . Enhanced ) != "" {
return templateResp . Enhanced , templateResp . Enhance , nil
}
if templateResp . ValidateErrorCount > 0 {
if len ( templateResp . ValidateError ) > 0 {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "validate: %s: at line %v" , templateResp . ValidateError [ 0 ] . Message , templateResp . ValidateError [ 0 ] . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( "validate: validation failed" ) . Build ( )
2023-06-21 18:49:40 +05:30
}
if templateResp . Error . Name != "" {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( templateResp . Error . Name ) . Build ( )
2023-06-21 18:49:40 +05:30
}
if strings . TrimSpace ( templateResp . Enhanced ) == "" && ! templateResp . Lint {
if templateResp . LintError . Reason != "" {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "lint: %s : at line %v" , templateResp . LintError . Reason , templateResp . LintError . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "lint: at line: %v" , templateResp . LintError . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( "template enhance failed" ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2024-04-16 19:27:51 +08:00
// formatTemplate formats template data using templateman format api
2023-06-21 18:49:40 +05:30
func formatTemplate ( data string ) ( string , bool , error ) {
resp , err := retryablehttp . DefaultClient ( ) . Post ( fmt . Sprintf ( "%s/format" , tmBaseUrl ) , "application/x-yaml" , strings . NewReader ( data ) )
if err != nil {
return data , false , err
}
if resp . StatusCode != 200 {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "unexpected status code: %v" , resp . Status ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
var templateResp TemplateResp
if err := json . NewDecoder ( resp . Body ) . Decode ( & templateResp ) ; err != nil {
return data , false , err
}
if strings . TrimSpace ( templateResp . Updated ) != "" {
return templateResp . Updated , templateResp . Format , nil
}
if templateResp . ValidateErrorCount > 0 {
if len ( templateResp . ValidateError ) > 0 {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "validate: %s: at line %v" , templateResp . ValidateError [ 0 ] . Message , templateResp . ValidateError [ 0 ] . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( "validate: validation failed" ) . Build ( )
2023-06-21 18:49:40 +05:30
}
if templateResp . Error . Name != "" {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( templateResp . Error . Name ) . Build ( )
2023-06-21 18:49:40 +05:30
}
if strings . TrimSpace ( templateResp . Updated ) == "" && ! templateResp . Lint {
if templateResp . LintError . Reason != "" {
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "lint: %s : at line %v" , templateResp . LintError . Reason , templateResp . LintError . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( fmt . Sprintf ( "lint: at line: %v" , templateResp . LintError . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return data , false , errkit . New ( "template format failed" ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2024-09-01 16:41:01 +08:00
// lintTemplate lints template data using templateman lint api
2023-06-21 18:49:40 +05:30
func lintTemplate ( data string ) ( bool , error ) {
resp , err := retryablehttp . DefaultClient ( ) . Post ( fmt . Sprintf ( "%s/lint" , tmBaseUrl ) , "application/x-yaml" , strings . NewReader ( data ) )
if err != nil {
return false , err
}
if resp . StatusCode != 200 {
2025-08-20 05:28:23 +05:30
return false , errkit . New ( fmt . Sprintf ( "unexpected status code: %v" , resp . Status ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
var lintResp TemplateLintResp
if err := json . NewDecoder ( resp . Body ) . Decode ( & lintResp ) ; err != nil {
return false , err
}
if lintResp . Lint {
return true , nil
}
if lintResp . LintError . Reason != "" {
2025-08-20 05:28:23 +05:30
return false , errkit . New ( fmt . Sprintf ( "lint: %s : at line %v" , lintResp . LintError . Reason , lintResp . LintError . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return false , errkit . New ( fmt . Sprintf ( "lint: at line: %v" , lintResp . LintError . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
// validateTemplate validates template data using templateman validate api
func validateTemplate ( data string ) ( bool , error ) {
resp , err := retryablehttp . DefaultClient ( ) . Post ( fmt . Sprintf ( "%s/validate" , tmBaseUrl ) , "application/x-yaml" , strings . NewReader ( data ) )
if err != nil {
return false , err
}
if resp . StatusCode != 200 {
2025-08-20 05:28:23 +05:30
return false , errkit . New ( fmt . Sprintf ( "unexpected status code: %v" , resp . Status ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
var validateResp TemplateResp
if err := json . NewDecoder ( resp . Body ) . Decode ( & validateResp ) ; err != nil {
return false , err
}
if validateResp . Validate {
return true , nil
}
if validateResp . ValidateErrorCount > 0 {
if len ( validateResp . ValidateError ) > 0 {
2025-08-20 05:28:23 +05:30
return false , errkit . New ( fmt . Sprintf ( "validate: %s: at line %v" , validateResp . ValidateError [ 0 ] . Message , validateResp . ValidateError [ 0 ] . Mark . Line ) ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return false , errkit . New ( "validate: validation failed" ) . Build ( )
2023-06-21 18:49:40 +05:30
}
if validateResp . Error . Name != "" {
2025-08-20 05:28:23 +05:30
return false , errkit . New ( validateResp . Error . Name ) . Build ( )
2023-06-21 18:49:40 +05:30
}
2025-08-20 05:28:23 +05:30
return false , errkit . New ( "template validation failed" ) . Build ( )
2023-06-21 18:49:40 +05:30
}
// parseAndAddMaxRequests parses and adds max requests to templates
func parseAndAddMaxRequests ( catalog catalog . Catalog , path , data string ) ( string , bool , error ) {
template , err := parseTemplate ( catalog , path )
if err != nil {
return data , false , err
}
if template . TotalRequests < 1 {
return data , false , nil
}
// Marshal the updated info block back to YAML.
infoBlockStart , infoBlockEnd := getInfoStartEnd ( data )
infoBlockOrig := data [ infoBlockStart : infoBlockEnd ]
infoBlockOrig = strings . TrimRight ( infoBlockOrig , "\n" )
infoBlock := InfoBlock { }
err = yaml . Unmarshal ( [ ] byte ( data ) , & infoBlock )
if err != nil {
return data , false , err
}
// if metadata is nil, create a new map
if infoBlock . Info . Metadata == nil {
infoBlock . Info . Metadata = make ( map [ string ] interface { } )
}
// do not update if it is already present and equal
if mr , ok := infoBlock . Info . Metadata [ "max-request" ] ; ok && mr . ( int ) == template . TotalRequests {
return data , false , nil
}
infoBlock . Info . Metadata [ "max-request" ] = template . TotalRequests
var newInfoBlock bytes . Buffer
yamlEncoder := yaml . NewEncoder ( & newInfoBlock )
yamlEncoder . SetIndent ( yamlIndentSpaces )
err = yamlEncoder . Encode ( infoBlock )
if err != nil {
return data , false , err
}
newInfoBlockData := strings . TrimSuffix ( newInfoBlock . String ( ) , "\n" )
// replace old info block with new info block
newTemplate := strings . ReplaceAll ( data , infoBlockOrig , newInfoBlockData )
err = os . WriteFile ( path , [ ] byte ( newTemplate ) , 0644 )
if err == nil {
return newTemplate , true , nil
}
return newTemplate , false , err
}
// parseTemplate parses a template and returns the template object
func parseTemplate ( catalog catalog . Catalog , templatePath string ) ( * templates . Template , error ) {
2025-07-09 14:47:26 -05:00
executorOpts := & protocols . ExecutorOptions {
2023-06-21 18:49:40 +05:30
Catalog : catalog ,
Options : defaultOpts ,
}
reader , err := executorOpts . Catalog . OpenFile ( templatePath )
if err != nil {
return nil , err
}
template , err := templates . ParseTemplateFromReader ( reader , nil , executorOpts )
if err != nil {
return nil , err
}
return template , nil
}
// find the start and end of the info block
func getInfoStartEnd ( data string ) ( int , int ) {
info := strings . Index ( data , "info:" )
var indices [ ] int
for _ , re := range allTagsRegex {
// find the first occurrence of the label
match := re . FindStringIndex ( data )
if match != nil {
indices = append ( indices , match [ 0 ] )
}
}
// find the first one after info block
sort . Ints ( indices )
return info , indices [ 0 ] - 1
}