diff --git a/pkg/model/read.go b/pkg/model/read.go index 056349cf..b4faa664 100644 --- a/pkg/model/read.go +++ b/pkg/model/read.go @@ -83,6 +83,11 @@ func ReadAndAnalyzeModel(config configReader, builtinRiskRules types.RiskRules, return nil, fmt.Errorf("unable to load model yaml: %w", loadError) } + return AnalyzeModel(modelInput, config, builtinRiskRules, customRiskRules, progressReporter) +} + +func AnalyzeModel(modelInput *input.Model, config configReader, builtinRiskRules types.RiskRules, customRiskRules types.RiskRules, progressReporter types.ProgressReporter) (*ReadResult, error) { + parsedModel, parseError := ParseModel(config, modelInput, builtinRiskRules, customRiskRules) if parseError != nil { return nil, fmt.Errorf("unable to parse model yaml: %w", parseError) diff --git a/pkg/server/execute.go b/pkg/server/execute.go index 200929da..bf1e1d0b 100644 --- a/pkg/server/execute.go +++ b/pkg/server/execute.go @@ -16,6 +16,9 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/threagile/threagile/pkg/input" + "github.com/threagile/threagile/pkg/model" + "github.com/threagile/threagile/pkg/risks" ) func (s *server) analyze(ginContext *gin.Context) { @@ -227,3 +230,46 @@ func (s *server) doItViaRuntimeCall(modelFile string, outputDir string, } } } + +func (s *server) editModelAnalyze(ginContext *gin.Context) { + defer func() { + var err error + if r := recover(); r != nil { + s.errorCount++ + err = r.(error) + log.Println(err) + ginContext.JSON(http.StatusBadRequest, gin.H{ + "error": strings.TrimSpace(err.Error()), + }) + } + }() + + var modelInput input.Model + if err := ginContext.ShouldBindJSON(&modelInput); err != nil { + ginContext.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid JSON provided", + }) + return + } + log.Printf("Received JSON: %+v\n", modelInput) + + progressReporter := DefaultProgressReporter{ + Verbose: s.config.GetVerbose(), + SuppressError: true, + } + customRiskRules := model.LoadCustomRiskRules(s.config.GetPluginFolder(), s.config.GetRiskRulePlugins(), progressReporter) + builtinRiskRules := risks.GetBuiltInRiskRules() + + result, err := model.AnalyzeModel(&modelInput, s.config, builtinRiskRules, customRiskRules, progressReporter) + if err != nil { + ginContext.JSON(http.StatusBadRequest, gin.H{ + "error": "Unable to analyze model: " + err.Error(), + }) + return + } + + ginContext.JSON(http.StatusOK, gin.H{ + "message": "JSON received successfully", + "data": result, + }) +} diff --git a/pkg/server/progress-reporter.go b/pkg/server/progress-reporter.go new file mode 100644 index 00000000..6284df2f --- /dev/null +++ b/pkg/server/progress-reporter.go @@ -0,0 +1,50 @@ +package server + +import ( + "fmt" + "log" +) + +type DefaultProgressReporter struct { + Verbose bool + SuppressError bool +} + +func (r DefaultProgressReporter) Info(a ...any) { + if r.Verbose { + fmt.Println(a...) + } +} + +func (DefaultProgressReporter) Warn(a ...any) { + fmt.Println(a...) +} + +func (r DefaultProgressReporter) Error(v ...any) { + if r.SuppressError { + r.Warn(v...) + return + } + log.Fatal(v...) +} + +func (r DefaultProgressReporter) Infof(format string, a ...any) { + if r.Verbose { + fmt.Printf(format, a...) + fmt.Println() + } +} + +func (DefaultProgressReporter) Warnf(format string, a ...any) { + fmt.Print("WARNING: ") + fmt.Printf(format, a...) + fmt.Println() +} + +func (r DefaultProgressReporter) Errorf(format string, v ...any) { + if r.SuppressError { + r.Warnf(format, v...) + return + } + log.Fatalf(format, v...) +} diff --git a/pkg/server/server.go b/pkg/server/server.go index d68c54b8..36d91e73 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -39,6 +39,9 @@ type serverConfigReader interface { GetDataAssetDiagramFilenameDOT() string GetReportFilename() string GetExcelRisksFilename() string + GetRiskExcelConfigHideColumns() []string + GetRiskExcelConfigSortByColumns() []string + GetRiskExcelConfigWidthOfColumns() map[string]float64 GetExcelTagsFilename() string GetJsonRisksFilename() string GetJsonTechnicalAssetsFilename() string @@ -170,6 +173,8 @@ func RunServer(config serverConfigReader, builtinRiskRules types.RiskRules) { router.GET("/meta/stats", s.stats) + router.POST("/edit-model/analyze", s.editModelAnalyze) + router.POST("/direct/analyze", s.analyze) router.POST("/direct/check", s.check) router.GET("/direct/stub", s.stubFile) diff --git a/server/static/edit-model.html b/server/static/edit-model.html index 0d3c736a..6fa7391a 100644 --- a/server/static/edit-model.html +++ b/server/static/edit-model.html @@ -20,6 +20,7 @@
+
@@ -42,7 +43,7 @@
-
+
diff --git a/server/static/js/edit-model.js b/server/static/js/edit-model.js index 573626d5..c85b33d8 100644 --- a/server/static/js/edit-model.js +++ b/server/static/js/edit-model.js @@ -86,6 +86,31 @@ $(document).ready(function() { } }); + $('#btnAnalyze').on('click', function() { + try { + $.ajax({ + url: "/edit-model/analyze", + type: "POST", + contentType: "application/json", + data: JSON.stringify(diagramYaml), + success: function(response) { + const risksByCategory = response.data.ParsedModel.generated_risks_by_category; + renderRiskTables(risksByCategory); + }, + error: function(jqXHR, textStatus, errorThrown) { + $("#riskAnalyzeContent").html("Error happened: " + jqXHR.responseJSON.error + "") + console.error("Request failed:", textStatus, errorThrown); + alert('Analysis failed'); + } + }); + + } catch (e) { + + console.error("Error analyzing model") + } + + }); + function nodeClicked(e, obj) { var evt = e.copy(); var node = obj.part; @@ -219,4 +244,56 @@ $(document).ready(function() { "data_assets_received": data_assets } } + + function renderRiskTables(generatedRisksByCategory) { + const container = $("#riskAnalyzeContent"); + container.empty(); // Clear any existing content + + // Iterate over each category in the risks object + $.each(generatedRisksByCategory, function(category, risks) { + // Create a table for the category + const table = $("").addClass("risk-table").css({ + border: "1px solid black", + width: "100%", + marginBottom: "20px" + }); + + // Add a caption for the category + const caption = $("").append( + $("").append( + $("
") + .text(`Category: ${category}`) + .css({ fontWeight: "bold", textAlign: "left", marginBottom: "10px" }); + table.append(caption); + + // Add a header row + const headerRow = $("
").text("Synthetic ID"), + $("").text("Title"), + $("").text("Severity"), + $("").text("Likelihood"), + $("").text("Impact"), + $("").text("Most Relevant Asset"), + $("").text("Communication Link"), + $("").text("Data Breach Assets") + ).css({ backgroundColor: "#f2f2f2", textAlign: "left" }); + table.append(headerRow); + + // Add a row for each risk + risks.forEach(risk => { + const row = $("
").text(risk.synthetic_id), + $("").html(risk.title), + $("").text(risk.severity), + $("").text(risk.exploitation_likelihood), + $("").text(risk.exploitation_impact), + $("").text(risk.most_relevant_technical_asset || "N/A"), + $("").text(risk.most_relevant_communication_link || "N/A"), + $("").text(risk.data_breach_technical_assets.join(", ") || "N/A") + ).css({ borderBottom: "1px solid black" }); + table.append(row); + }); + + // Append the table to the container + container.append(table); + }); +} }); diff --git a/server/static/js/property-editor.js b/server/static/js/property-editor.js index de068153..baed994a 100644 --- a/server/static/js/property-editor.js +++ b/server/static/js/property-editor.js @@ -37,7 +37,9 @@ class EditorGenerator { this.object[key] = input.val(); callback(key, input.val()); }); - input.datepicker(); + input.datepicker({ + dateFormat: "yy-mm-dd" // ISO 8601 format + }); } else if (property.enum) { input = $('