diff --git a/.agents/skills/release-notes/SKILL.md b/.agents/skills/release-notes/SKILL.md new file mode 100644 index 00000000..090599dd --- /dev/null +++ b/.agents/skills/release-notes/SKILL.md @@ -0,0 +1,198 @@ +--- +name: release-notes +description: Draft release notes for the next FACTION release by reading commit diffs since the previous tag and pushing them to GitHub as a DRAFT release via `gh release create --draft` (or updating an existing draft). Use when the user asks to draft/prepare release notes, summarize what's changed since a tag, or prepare a release writeup. Never publishes — always draft. +--- + +# FACTION release notes + +You are drafting human-quality release notes for the FACTION project. The goal +is the kind of writeup a user reads in a GitHub Release and immediately +understands what changed, why it matters to them, and whether they need to +do anything to upgrade. + +Keyword-matching commit subjects produces shallow output. **Read the diffs.** +That is the difference between "Add default-vulnerability CRUD endpoints" (a +commit subject) and a release-notes paragraph that lists each new endpoint, +its method/path, and the round-trip-cleanly fix for names containing `/`. + +## Step 1 — Pick the version + +If the user passed a version (e.g. `/release-notes 1.8.6`), use it. + +Otherwise derive it from `pom.xml`: + +```bash +grep -m1 '' pom.xml | sed -E 's/.*([^<]+)<\/version>.*/\1/' | sed 's/-SNAPSHOT//' +``` + +If the result still contains `SNAPSHOT` or is empty, stop and ask the user +which version this release should be tagged as. + +## Step 2 — Pick the previous tag + +Default to the most recent annotated/lightweight tag: + +```bash +git describe --tags --abbrev=0 +``` + +If that errors (no tags exist), ask the user for a starting ref instead of +guessing. If the user passed a second arg, use that as the previous tag. + +## Step 3 — Inspect what changed + +Run these (in parallel where independent): + +```bash +git log --reverse ..HEAD --pretty=format:'%h %s' +git diff --stat ..HEAD +``` + +Then **for each non-trivial commit**, read the actual diff: + +```bash +git show --stat +git show -- +``` + +Skip commits whose subject starts with `[maven-release-plugin]` — they are +version-bump noise. Also skip pure-internal hygiene (gitignore tweaks, +formatting-only changes) unless that's all that landed. + +If `.github/release.yml` exists, read it — its `categories` section names +match the GitHub Release auto-categorizer (currently +`🎉 🚀 Upgrades 🎉 🚀` and `🐛 Bugfixes 🐛`). Use those exact section +titles so the file mirrors what GitHub would generate. + +## Step 4 — Draft on GitHub + +You will push a draft release to GitHub rather than committing a markdown +file to the repo. The user reviews and publishes from +. + +Preflight: + +```bash +gh auth status # verify gh is installed and logged in +gh release view # check if a release for this version already exists +``` + +- If `gh auth status` fails, stop and tell the user to run `gh auth login`. +- If `gh release view` succeeds **and** the release is published (not a + draft), stop and ask before overwriting — published releases should not be + silently mutated. +- If `gh release view` succeeds and the release **is** a draft, you'll + update it in place via `gh release edit` (see below). +- If `gh release view` fails with "release not found", you'll create a new + draft via `gh release create`. + +Compose the notes body using the structure below, then write it to a temp +file (`/tmp/release-notes-.md`) — passing markdown via `--notes` +inline is fragile with backticks and code fences, so always use +`--notes-file`. + +Body structure (omit any section that's empty): + +The body starts directly with the executive summary — do **not** add a +`# FACTION ` title or a `_Release date: ..._` line. GitHub already +renders the release title and date from the release object itself. + +```markdown + + +## 🎉 🚀 Upgrades 🎉 🚀 + +### + + + +## 🐛 Bugfixes 🐛 + +- + +## 🧰 Internal / Test infrastructure + + + +## Upgrade notes + +- **Database migration:** required / not required (state which). +- **API:** call out any breaking changes, or explicitly say "all existing + endpoints continue to work unchanged" if true. +- **Configuration:** any new required env vars or settings. +- **Permissions:** any new permission scopes or role changes. + +## Full changelog + +...> +``` + +## Style rules + +- **Read diffs, don't paraphrase commit subjects.** A commit titled "fix bug" + tells the reader nothing; the diff tells you what actually changed. +- **User-facing voice.** "You can now …" beats "We added a method that …". +- **Group by feature, not by commit.** Three commits that together + implement one endpoint become one bullet. +- **Tables for API additions.** Method / path / purpose. Always. +- **Backward compatibility is load-bearing.** If old clients keep working, + say so explicitly — that's often the most reassuring sentence in the file. +- **No emojis inside body text.** They're fine in the section headers + (because `.github/release.yml` uses them), but don't sprinkle them through + the prose. +- **No "Co-Authored-By" anywhere.** This project's commits and release + notes never carry that trailer. + +## Step 5 — Push the draft + +Write the composed body to the temp file, then create or update the draft. + +**New draft** (no existing release for ``): + +```bash +gh release create \ + --draft \ + --title "FACTION " \ + --notes-file /tmp/release-notes-.md \ + --target main +``` + +`--draft` is mandatory — never publish from this skill. The tag does not +have to exist yet; GitHub creates it at `--target` only when the draft is +published. + +**Existing draft** (re-running the skill to iterate on the same release): + +```bash +gh release edit \ + --draft \ + --notes-file /tmp/release-notes-.md +``` + +`gh release edit` keeps the existing title unless `--title` is also passed. +Only pass `--title` if the user explicitly asked to rename the release. + +## Hand-off + +After the `gh` command succeeds: +1. Print the draft URL. `gh release create` prints it on stdout; for the + edit path, get it with `gh release view --json url -q .url`. +2. Print the first ~10 lines of the body so the user can sanity-check the + framing without leaving the terminal. +3. Tell the user the draft is **unpublished** and that they should review + on GitHub before clicking Publish. +4. Do **not** publish, `git tag`, or `git push` anything. The skill's + contract ends at "draft is up for review." + +## When gh isn't available + +If `gh` is not installed or the user is offline, fall back to writing +`RELEASE_NOTES_.md` at the repo root and tell the user explicitly +that you did so because GitHub was unreachable — they can paste it into +the Releases UI manually. diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000..0764a76d --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,12 @@ +# Used by CodeQL default setup via the repository property `github-codeql-config-file`. +# Third-party and generated front-end code is excluded: findings there belong upstream +# (and are re-raised at new line numbers on every library upgrade). Application code in +# src/, test/, WebContent/src and WebContent/WEB-INF stays fully analyzed. +name: "FACTION CodeQL config" +paths-ignore: + - WebContent/plugins + - WebContent/bootstrap + - WebContent/fileupload + - WebContent/dist + - WebContent/src/scripts + - WebContent/node_modules diff --git a/.github/workflows/assign.yml b/.github/workflows/assign.yml index acedb456..700d0ac4 100644 --- a/.github/workflows/assign.yml +++ b/.github/workflows/assign.yml @@ -4,6 +4,8 @@ on: issues: types: [opened] +permissions: {} + jobs: auto-assign: runs-on: ubuntu-latest diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index ff89f4f7..0288c02b 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -11,6 +11,8 @@ on: # random HH:MM to avoid a load spike on GitHub Actions at 00:00 - cron: 33 8 * * * name: Semgrep +permissions: + contents: read jobs: semgrep: name: semgrep/ci diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 73c84455..3860728b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,9 @@ concurrency: group: tests-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: test: name: mvn test diff --git a/.gitignore b/.gitignore index 1f8f014c..c7794139 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ vtrack-os/.project .zed/ logs/ + +# Local AI-tool config (contains API keys) +.codex/ diff --git a/WebContent/WEB-INF/jsp/assessment/list.jsp b/WebContent/WEB-INF/jsp/assessment/list.jsp index 4ff45c37..4e3d4597 100644 --- a/WebContent/WEB-INF/jsp/assessment/list.jsp +++ b/WebContent/WEB-INF/jsp/assessment/list.jsp @@ -181,7 +181,8 @@ color:#00a65a + + --> diff --git a/WebContent/WEB-INF/jsp/client/Access.jsp b/WebContent/WEB-INF/jsp/client/Access.jsp index 8b9a0da1..585511b5 100644 --- a/WebContent/WEB-INF/jsp/client/Access.jsp +++ b/WebContent/WEB-INF/jsp/client/Access.jsp @@ -88,8 +88,9 @@ - - + + + diff --git a/WebContent/WEB-INF/jsp/client/Client.jsp b/WebContent/WEB-INF/jsp/client/Client.jsp index 570fab43..92af84a4 100644 --- a/WebContent/WEB-INF/jsp/client/Client.jsp +++ b/WebContent/WEB-INF/jsp/client/Client.jsp @@ -98,8 +98,9 @@ - - + + + diff --git a/WebContent/WEB-INF/jsp/cms/ReportingTemplates.jsp b/WebContent/WEB-INF/jsp/cms/ReportingTemplates.jsp index 2aa6a1af..4caae386 100644 --- a/WebContent/WEB-INF/jsp/cms/ReportingTemplates.jsp +++ b/WebContent/WEB-INF/jsp/cms/ReportingTemplates.jsp @@ -92,7 +92,7 @@ - + diff --git a/WebContent/WEB-INF/jsp/cms/TemplateUpload.jsp b/WebContent/WEB-INF/jsp/cms/TemplateUpload.jsp index 9b1921a0..293a6347 100644 --- a/WebContent/WEB-INF/jsp/cms/TemplateUpload.jsp +++ b/WebContent/WEB-INF/jsp/cms/TemplateUpload.jsp @@ -36,10 +36,10 @@ - + - + diff --git a/WebContent/WEB-INF/jsp/cms/templates.jsp b/WebContent/WEB-INF/jsp/cms/templates.jsp index dcb31ede..0b03c207 100644 --- a/WebContent/WEB-INF/jsp/cms/templates.jsp +++ b/WebContent/WEB-INF/jsp/cms/templates.jsp @@ -35,7 +35,7 @@ height: 700px; < - ${name }${team.teamName} + diff --git a/WebContent/WEB-INF/jsp/header.jsp b/WebContent/WEB-INF/jsp/header.jsp index 1a159d60..14b9a996 100644 --- a/WebContent/WEB-INF/jsp/header.jsp +++ b/WebContent/WEB-INF/jsp/header.jsp @@ -25,8 +25,9 @@ - - + + + @@ -57,7 +58,7 @@   ${_title1} ${_title2} + src="../tri-logo.png" />   diff --git a/WebContent/WEB-INF/jsp/peerreviews/TrackChanges.jsp b/WebContent/WEB-INF/jsp/peerreviews/TrackChanges.jsp index 50cda270..701fc003 100644 --- a/WebContent/WEB-INF/jsp/peerreviews/TrackChanges.jsp +++ b/WebContent/WEB-INF/jsp/peerreviews/TrackChanges.jsp @@ -133,7 +133,7 @@ span.Informational {

- Peer Review for [${asmt.appId}] - ${asmt.name } - ${asmt.assessor[0].fname} ${asmt.assessor[0].lname} + Peer Review for [] - -

@@ -168,7 +168,7 @@ span.Informational {
- +
Notes
@@ -189,7 +189,7 @@ span.Informational {
- +
Notes
@@ -260,7 +260,7 @@ span.Informational {
Description
- +
Description Notes
@@ -276,7 +276,7 @@ span.Informational {
Recommendation
- +
Recommendation Notes
@@ -292,7 +292,7 @@ span.Informational {
Details
- +
Detail Notes
@@ -375,7 +375,7 @@ span.Informational { - + Incomplete diff --git a/WebContent/WEB-INF/jsp/register/newuser.jsp b/WebContent/WEB-INF/jsp/register/newuser.jsp index 736806ff..bceeb7e1 100644 --- a/WebContent/WEB-INF/jsp/register/newuser.jsp +++ b/WebContent/WEB-INF/jsp/register/newuser.jsp @@ -71,7 +71,9 @@ User Name and/or Password is invalid - + + + + + + @@ -262,7 +263,7 @@ content: content }); - }).error(function(){ + }).fail(function(){ $.alert({ title: 'Error!', content: "There is a problem with your request." diff --git a/mise.toml b/mise.toml index 3b3c8438..e50399d2 100644 --- a/mise.toml +++ b/mise.toml @@ -92,7 +92,7 @@ run = "docker-compose -f docker-compose-hotreload.yml restart tomcat-service" [tasks."hotreload:logs"] description = "HotReload: View Logs" -run = "docker-compose -f docker-compose-hotreload.yml logs -f tomcat-service" +run = "docker compose -f docker-compose-hotreload.yml logs -f tomcat-service" [tasks."hotreload:stop"] description = "HotReload: Stop" diff --git a/pom.xml b/pom.xml index b2a25fa5..e9db1046 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.faction faction - 1.8.13-SNAPSHOT + 1.8.14-SNAPSHOT war Faction diff --git a/src/com/fuse/actions/admin/Options.java b/src/com/fuse/actions/admin/Options.java index 4dd13311..96a8a072 100644 --- a/src/com/fuse/actions/admin/Options.java +++ b/src/com/fuse/actions/admin/Options.java @@ -129,6 +129,10 @@ public String execute() { this._message = "Name is Empty"; return this.ERRORJSON; } + if (FSUtils.containsHTML(this.name)) { + this._message = "Assessment Type name cannot contain HTML"; + return this.ERRORJSON; + } AssessmentType AT = AssessmentQueries.getAssessmentTypeByName(em, this.name); if (AT != null) { @@ -544,12 +548,21 @@ public String updateTiltes() { if (!this.testToken(false)) return this.ERRORJSON; + if (this.title == null || this.title.length < 2) { + this._message = "Both titles are required"; + return this.ERRORJSON; + } + if (FSUtils.containsHTML(this.title[0]) || FSUtils.containsHTML(this.title[1])) { + this._message = "Titles cannot contain HTML"; + return this.ERRORJSON; + } + EMS = (SystemSettings) em.createQuery("from SystemSettings").getResultList().stream().findFirst().orElse(null); if (EMS == null) { EMS = new SystemSettings(); } - EMS.setBoldTitle(this.title[0]); - EMS.setOtherTitle(this.title[1]); + EMS.setBoldTitle(this.title[0].trim()); + EMS.setOtherTitle(this.title[1].trim()); HibHelper.getInstance().preJoin(); em.joinTransaction(); em.persist(EMS); @@ -634,6 +647,14 @@ public String editType() { this._message = "Assessment Type does not exist"; return this.ERRORJSON; } + if (this.getName() == null || this.getName().trim().equals("")) { + this._message = "Name is Empty"; + return this.ERRORJSON; + } + if (FSUtils.containsHTML(this.getName())) { + this._message = "Assessment Type name cannot contain HTML"; + return this.ERRORJSON; + } AssessmentType t2 = AssessmentQueries.getAssessmentTypeByName(em, this.getName()); diff --git a/src/com/fuse/actions/appstore/AppStoreController.java b/src/com/fuse/actions/appstore/AppStoreController.java index 114137e6..254a8a3d 100644 --- a/src/com/fuse/actions/appstore/AppStoreController.java +++ b/src/com/fuse/actions/appstore/AppStoreController.java @@ -80,6 +80,7 @@ public String enableApp() { HibHelper.getInstance().preJoin(); em.joinTransaction(); em.persist(app); + AuditLog.audit(this, "Extension enabled: " + app.getName() + " " + app.getVersion(), AuditLog.UserAction, false); HibHelper.getInstance().commit(); } _result="success"; @@ -95,6 +96,7 @@ public String disableApp() { HibHelper.getInstance().preJoin(); em.joinTransaction(); em.persist(app); + AuditLog.audit(this, "Extension disabled: " + app.getName() + " " + app.getVersion(), AuditLog.UserAction, false); HibHelper.getInstance().commit(); _result="success"; return MESSAGEJSON; diff --git a/src/com/fuse/actions/appstore/InstallExtensionController.java b/src/com/fuse/actions/appstore/InstallExtensionController.java index b110da03..ab929c87 100644 --- a/src/com/fuse/actions/appstore/InstallExtensionController.java +++ b/src/com/fuse/actions/appstore/InstallExtensionController.java @@ -16,6 +16,7 @@ import com.fuse.actions.FSActionSupport; import com.fuse.dao.AppStore; +import com.fuse.utils.FSUtils; import com.fuse.dao.AuditLog; import com.fuse.dao.HibHelper; import com.opensymphony.xwork2.interceptor.annotations.Before; @@ -73,9 +74,15 @@ public String uploadUpdate() throws IOException, ParseException { .stream() .findFirst() .orElse(null); - - FileInputStream fis = new FileInputStream(file_data); - app.updateApp(fis); + if (app == null || file_data == null) { + return this.errorJson("Extension not found or no file uploaded"); + } + try (FileInputStream fis = new FileInputStream(FSUtils.checkUploadedFile(file_data))) { + app.updateApp(fis); + } catch (Exception ex) { + AuditLog.audit(this, "Rejected extension update upload: " + ex.getMessage(), AuditLog.UserAction, true); + return this.errorJson("Invalid extension JAR: " + ex.getMessage()); + } String json = app.getMeta(); ServletActionContext.getRequest().getSession().setAttribute("PreviewApp", app); stream = new ByteArrayInputStream(json.toString().getBytes()); @@ -89,19 +96,37 @@ public String uploadUpdate() throws IOException, ParseException { @Result(name = "input", location = "/WEB-INF/jsp/uploadError.jsp") }) public String uploadFile() throws IOException, ParseException { - FileInputStream fis = new FileInputStream(file_data); + if (file_data == null) { + return this.errorJson("No file uploaded"); + } AppStore preview = new AppStore(); - preview.parseJar(fis); + try (FileInputStream fis = new FileInputStream(FSUtils.checkUploadedFile(file_data))) { + preview.parseJar(fis); + } catch (Exception ex) { + AuditLog.audit(this, "Rejected extension upload: " + ex.getMessage(), AuditLog.UserAction, true); + return this.errorJson("Invalid extension JAR: " + ex.getMessage()); + } String json = preview.getMeta(); ServletActionContext.getRequest().getSession().setAttribute("PreviewApp", preview); stream = new ByteArrayInputStream(json.toString().getBytes()); return "json"; } + + private String errorJson(String message) { + String json = "{\"error\": \"" + message.replace("\\", "\\\\").replace("\"", "\\\"") + "\"}"; + stream = new ByteArrayInputStream(json.getBytes()); + return "json"; + } @Action(value = "InstallApp") public String installApp() throws IOException, ParseException { AppStore app = (AppStore) ServletActionContext.getRequest().getSession().getAttribute("PreviewApp"); + if (app == null) { + _message="No extension has been uploaded for review"; + _result="error"; + return MESSAGEJSON; + } Boolean alreadyInstalled =em.createQuery("from AppStore where hash = :hash") .setParameter("hash", app.getHash()) .getResultList() @@ -116,7 +141,10 @@ public String installApp() throws IOException, ParseException { HibHelper.getInstance().preJoin(); em.joinTransaction(); em.persist(app); + AuditLog.audit(this, "Extension installed: " + app.getName() + " " + app.getVersion() + " (hash " + app.getHash() + ")", + AuditLog.UserAction, false); HibHelper.getInstance().commit(); + ServletActionContext.getRequest().getSession().removeAttribute("PreviewApp"); _result="success"; return MESSAGEJSON; @@ -125,6 +153,11 @@ public String installApp() throws IOException, ParseException { @Action(value = "UpdateApp") public String updateApp() throws IOException, ParseException { AppStore app = (AppStore) ServletActionContext.getRequest().getSession().getAttribute("PreviewApp"); + if (app == null) { + _message="No extension has been uploaded for review"; + _result="error"; + return MESSAGEJSON; + } List apps =em.createQuery("from AppStore where hash = :hash or uuid = :uuid") .setParameter("hash", app.getHash()) .setParameter("uuid", app.getUuid()) @@ -156,7 +189,10 @@ public String updateApp() throws IOException, ParseException { HibHelper.getInstance().preJoin(); em.joinTransaction(); em.merge(app); + AuditLog.audit(this, "Extension updated: " + app.getName() + " " + app.getVersion() + " (hash " + app.getHash() + ")", + AuditLog.UserAction, false); HibHelper.getInstance().commit(); + ServletActionContext.getRequest().getSession().removeAttribute("PreviewApp"); _result="success"; return MESSAGEJSON; diff --git a/src/com/fuse/actions/assessment/UploadReport.java b/src/com/fuse/actions/assessment/UploadReport.java index 690251de..c21d68a1 100644 --- a/src/com/fuse/actions/assessment/UploadReport.java +++ b/src/com/fuse/actions/assessment/UploadReport.java @@ -10,6 +10,7 @@ import org.apache.struts2.convention.annotation.Namespace; import com.fuse.actions.FSActionSupport; +import com.fuse.utils.FSUtils; import com.fuse.dao.Assessment; import com.fuse.dao.AuditLog; import com.fuse.dao.FinalReport; @@ -71,7 +72,7 @@ public String uploadReport() throws Exception { return this.ERRORJSON; } - byte[] fileBytes = Files.readAllBytes(uploadReport.toPath()); + byte[] fileBytes = Files.readAllBytes(FSUtils.checkUploadedFile(uploadReport).toPath()); String b64 = Base64.encodeBase64String(fileBytes); String fileType = isPdf ? "pdf" : "docx"; diff --git a/src/com/fuse/dao/AppStore.java b/src/com/fuse/dao/AppStore.java index b340fcf1..75d18a72 100644 --- a/src/com/fuse/dao/AppStore.java +++ b/src/com/fuse/dao/AppStore.java @@ -296,6 +296,9 @@ public void parseJar(FileInputStream fis) throws IOException, ParseException { JarInputStream jarStream = new JarInputStream(fis); Manifest manifest = jarStream.getManifest(); + if (manifest == null) { + throw new IOException("Extension JAR is missing META-INF/MANIFEST.MF"); + } Attributes attr = manifest.getMainAttributes(); String title = attr.getValue("Title"); String author = attr.getValue("Author"); @@ -338,22 +341,28 @@ public void parseJar(FileInputStream fis) throws IOException, ParseException { logo.write(data, 0, size); } } - if (!entry.isDirectory() && entry.getName().endsWith("com.faction.extender.ApplicationInventory")) { + if (!entry.isDirectory() && entry.getName().equals("META-INF/services/com.faction.extender.ApplicationInventory")) { isInventoryApp = true; } - if (!entry.isDirectory() && entry.getName().endsWith("com.faction.extender.AssessmentManager")) { + if (!entry.isDirectory() && entry.getName().equals("META-INF/services/com.faction.extender.AssessmentManager")) { isAssessmentApp = true; } - if (!entry.isDirectory() && entry.getName().endsWith("com.faction.extender.ReportManager")) { + if (!entry.isDirectory() && entry.getName().equals("META-INF/services/com.faction.extender.ReportManager")) { isReportingApp=true; } - if (!entry.isDirectory() && entry.getName().endsWith("com.faction.extender.VulnerabilityManager")) { + if (!entry.isDirectory() && entry.getName().equals("META-INF/services/com.faction.extender.VulnerabilityManager")) { isVulnerabilityApp = true; } - if (!entry.isDirectory() && entry.getName().endsWith("com.faction.extender.VerificationManager")) { + if (!entry.isDirectory() && entry.getName().equals("META-INF/services/com.faction.extender.VerificationManager")) { isVerificationApp = true; } } + if (!(isAssessmentApp || isVerificationApp || isVulnerabilityApp || isInventoryApp || isReportingApp)) { + throw new IOException("Extension JAR does not register any FACTION extension service (META-INF/services/com.faction.extender.*)"); + } + if (title == null || author == null || version == null || url == null) { + throw new IOException("Extension JAR manifest must define Title, Author, Version and URL"); + } //reset file pointer FileChannel fc = fis.getChannel(); fc.position(0); diff --git a/src/com/fuse/dao/Comment.java b/src/com/fuse/dao/Comment.java index e45edb4c..5ca59459 100644 --- a/src/com/fuse/dao/Comment.java +++ b/src/com/fuse/dao/Comment.java @@ -1,5 +1,7 @@ package com.fuse.dao; +import com.fuse.utils.FSUtils; + import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -128,11 +130,11 @@ public void setSummary2(String summary2) { } public void setSummary1_notes(String summary1_notes) { - this.summary1_notes = summary1_notes; + this.summary1_notes = FSUtils.sanitizeHTML(summary1_notes); } public void setSummary2_notes(String summary2_notes) { - this.summary2_notes = summary2_notes; + this.summary2_notes = FSUtils.sanitizeHTML(summary2_notes); } public List getCommenters() { diff --git a/src/com/fuse/dao/Vulnerability.java b/src/com/fuse/dao/Vulnerability.java index abb0449f..1f13bd93 100644 --- a/src/com/fuse/dao/Vulnerability.java +++ b/src/com/fuse/dao/Vulnerability.java @@ -274,7 +274,7 @@ public String getDesc_notes() { } @Transient public void setDesc_notes(String desc_notes) { - this.desc_notes = desc_notes; + this.desc_notes = FSUtils.sanitizeHTML(desc_notes); } @Transient public String getRec_notes() { @@ -282,7 +282,7 @@ public String getRec_notes() { } @Transient public void setRec_notes(String rec_notes) { - this.rec_notes = rec_notes; + this.rec_notes = FSUtils.sanitizeHTML(rec_notes); } public String getCvssScore() { return cvssScore == null? "" : cvssScore; @@ -329,7 +329,7 @@ public String getDetail_notes() { } @Transient public void setDetail_notes(String detail_notes) { - this.detail_notes = detail_notes; + this.detail_notes = FSUtils.sanitizeHTML(detail_notes); } public Boolean getDesc_lock() { return desc_lock; diff --git a/src/com/fuse/extenderapi/Extensions.java b/src/com/fuse/extenderapi/Extensions.java index b821afb1..1c542bb7 100644 --- a/src/com/fuse/extenderapi/Extensions.java +++ b/src/com/fuse/extenderapi/Extensions.java @@ -575,8 +575,14 @@ public void loadExtensions() throws MalformedURLException { List apps = this.sortApps(); for (AppStore app : apps) { - + // sortApps() only returns extensions that are enabled *and* registered for this + // event type, so bytecode from a disabled (or unrelated) extension is never + // loaded into the JVM. ServiceLoader runs static initialisers and constructors + // as soon as it iterates, so the filtering has to happen before this point. URLClassLoader extensionLoader = dynamicExtensionClassLoader(app); + if (extensionLoader == null) { + continue; + } ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); // Load Assessment Manager Extensions @@ -667,16 +673,40 @@ public void loadExtensions() throws MalformedURLException { } } + /** + * Installed extensions that are enabled and have opted in to the event type this + * instance serves, in configured order. Disabled extensions are never returned and + * therefore never class-loaded. + */ private List sortApps() { EntityManager em = entityManagerFactory.createEntityManager(); try { List apps = em.createQuery("from AppStore order by order").getResultList(); - return apps; + return apps.stream() + .filter(app -> Boolean.TRUE.equals(app.getEnabled()) && this.isEnabledFor(app)) + .collect(Collectors.toList()); } finally { em.close(); } } + private boolean isEnabledFor(AppStore app) { + switch (this.type) { + case ASMT_MANAGER: + return Boolean.TRUE.equals(app.getAssessmentEnabled()); + case REPORT_MANAGER: + return Boolean.TRUE.equals(app.getReportEnabled()); + case VULN_MANAGER: + return Boolean.TRUE.equals(app.getVulnerabilityEnabled()); + case VER_MANAGER: + return Boolean.TRUE.equals(app.getVerificationEnabled()); + case INVENTORY: + return Boolean.TRUE.equals(app.getInventoryEnabled()); + default: + return false; + } + } + public List getLogs() { return this.logs; diff --git a/src/com/fuse/utils/FSUtils.java b/src/com/fuse/utils/FSUtils.java index 675dd1de..f35b2b75 100644 --- a/src/com/fuse/utils/FSUtils.java +++ b/src/com/fuse/utils/FSUtils.java @@ -1,940 +1,1002 @@ -package com.fuse.utils; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.Authenticator; -import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.PasswordAuthentication; -import java.net.Proxy; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.spec.KeySpec; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Scanner; -import java.util.TreeMap; -import java.util.UUID; -import java.util.jar.Manifest; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; -import javax.persistence.EntityManager; -import javax.servlet.ServletContext; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.io.IOUtils; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.owasp.html.HtmlPolicyBuilder; -import org.owasp.html.PolicyFactory; -import org.w3c.tidy.Tidy; - -import com.fuse.dao.Assessment; -import com.fuse.dao.Category; -import com.fuse.dao.CustomField; -import com.fuse.dao.CustomType; -import com.fuse.dao.DefaultVulnerability; -import com.fuse.dao.HibHelper; -import com.fuse.dao.ReportOptions; -import com.fuse.dao.RiskLevel; -import com.fuse.dao.Vulnerability; -import com.fuse.dao.CustomType; - -import org.commonmark.Extension; -import org.commonmark.ext.autolink.AutolinkExtension; -import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; -import org.commonmark.ext.gfm.tables.TablesExtension; -import org.commonmark.ext.ins.InsExtension; -import org.commonmark.node.*; -import org.commonmark.parser.IncludeSourceSpans; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.html.HtmlRenderer; - -public class FSUtils { - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); - - public static String generatePasswd() { - String chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - StringBuilder sb = new StringBuilder(32); - for (int i = 0; i < 32; i++) { - sb.append(chars.charAt(SECURE_RANDOM.nextInt(chars.length()))); - } - return sb.toString(); - } - - private static String INPUT = "Unvalidated Input"; - private static String SERVER = "Server Misconfiguration"; - private static String CRYPTO = "Weak Cryptography"; - private static String DATAEX = "Data Exposure"; - private static String ACCESS = "Broken Access Control and Session Management"; - private static String PUBLIC = "Publicly Known Vulnerability"; - private static String OUTDATED = "Outdated Libraries and Components"; - private static String UNKNOWN = "Uncategorized"; - - public static String jtidy(String html) { - - MethodProfiler.ProfileContext context = MethodProfiler.start("FSUtils", "jtidy"); - try { - // figures seems to kill the whole message. - //html = html.replaceAll("<(/)?figure>", ""); - if(!html.contains("<")) { - return html; - } - - Tidy tidy = new Tidy(); - InputStream stream = new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8)); - // tidy.setXmlOut(true); - tidy.setQuiet(true); - tidy.setWord2000(false); - tidy.setQuoteAmpersand(true); - tidy.setQuoteMarks(true); - tidy.setQuoteNbsp(true); - tidy.setTidyMark(false); - tidy.setShowErrors(0); - tidy.setShowWarnings(false); - tidy.setWraplen(0); - tidy.setWrapAttVals(false); - tidy.setPrintBodyOnly(true); - tidy.setXHTML(true); - - tidy.setOutputEncoding("UTF-8"); - tidy.setInputEncoding("UTF-8"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - tidy.parse(stream, baos); - try { - String out = new String(baos.toByteArray(), "UTF-8"); - out = out.replaceAll(" ", " "); - ArrayList updated = new ArrayList(); - Boolean preSection=false; - for (String line : out.split("\n")) { - //line = line.replaceAll("^[ ]+", ""); - if(line.contains("")) { - preSection = true; - } - if (line.contains("")) { - preSection = false; - } - if(preSection) { - line = line + "\n"; - } - updated.add(line); - } - out = String.join("", updated); - - return out; - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - return new String(baos.toByteArray()); - } - }finally { - context.end(); - } - } - - public static String sanitizeHTML(String html) { - PolicyFactory policyBuilder = new HtmlPolicyBuilder().allowAttributes("src").onElements("img") - .allowUrlProtocols("data", "http", "https").allowAttributes("href").onElements("a") - .allowAttributes("src", "width", "height", "controls").onElements("video") - .allowAttributes("style", "class", "colspan").onElements("a", "label", "h1", "h2", "h3", "h4", "h5", "h6", "p", "i", "b", "u", "strong", "em", - "small", "big", "pre", "code", "cite", "samp", "sub", "sup", "strike", "center", "blockquote", - "hr", "br", "col", "font", "div", "img", "ul", "ol", "li", "dd", "dt", "dl", "tbody", "thead", - "tfoot", "table", "td", "th", "tr", "colgroup", "fieldset", "legend", "span", "backquote") - .allowAttributes("data-changedata", "data-cid", "data-last-change-time", "data-time", "data-userid", - "data-username", "title").onElements("span") - .allowAttributes("border", "cellpadding", "cellspacing", "style", "class", "colspan").onElements("table") - .allowStandardUrlProtocols() - .allowElements("a", "label", "h1", "h2", "h3", "h4", "h5", "h6", "p", "i", "b", "u", "strong", "em", - "small", "big", "pre", "code", "cite", "samp", "sub", "sup", "strike", "center", "blockquote", - "hr", "br", "col", "font", "div", "img", "ul", "ol", "li", "dd", "dt", "dl", "tbody", "thead", - "tfoot", "table", "td", "th", "tr", "colgroup", "fieldset", "legend", "del", "ins", "figure", - "span", "figcaption", "ins") - .toFactory(); - String sanitized = policyBuilder.sanitize(html); - - /// Adding regex for MS Word table copy and paste that mess everything up. - return sanitized.replaceAll("(style=\".*? )(windowtext)", "$1").replaceAll("style=\"line-height:normal\"", ""); - - } - - public static String sanitizeGUID(String guid) { - String regexGUID = "^[0-9a-zA-Z\\-].*$"; - Matcher guids = Pattern.compile(regexGUID).matcher(guid); - if (guids.find()) - return guids.group(); - else - return ""; - - } - - public static String sanitizeMongo(String input) { - if (input == null) return ""; - return input.replaceAll("([+?*^${}()|\\[\\]\\\\])", "\\\\$1"); - } - - public static boolean checkEmail(String email) { - String emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"; - Matcher match = Pattern.compile(emailRegex).matcher(email); - return match.matches(); - - } - - public static List sortVulns(List vulns) { - if (vulns == null) - return new ArrayList(); - - TreeMap sorted = new TreeMap(); - for (Vulnerability v : vulns) { - sorted.put(v.getOverall(), v); - } - return new ArrayList(sorted.values()); - - } - - public static List sortUniqueAssessment(List asmts) { - if (asmts == null) - return new ArrayList(); - TreeMap sorted = new TreeMap(); - for (Assessment a : asmts) { - sorted.put(a.getAppId(), a); - } - return new ArrayList(sorted.values()); - - } - - public static void importVulnDB(String proxyurl, int port, String username, String password, EntityManager em) - throws IOException, ParseException { - - // Create the default Categories - createCategories(em); - // Load external resource of vulnerability data. - String vdbzip = "https://codeload.github.com/factionsecurity/data/zip/master"; - URL obj = new URL(vdbzip); - HttpURLConnection conn = null; - if (proxyurl != null) { - - Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyurl, port)); - if (username != null) { - Authenticator authenticator = new Authenticator() { - - public PasswordAuthentication getPasswordAuthentication() { - return (new PasswordAuthentication("user", "password".toCharArray())); - } - }; - Authenticator.setDefault(authenticator); - } - conn = (HttpURLConnection) obj.openConnection(proxy); - } else { - - conn = (HttpURLConnection) obj.openConnection(); - } - - InputStream is = conn.getInputStream(); - - // Unzip the files into memory - ZipInputStream zis = new ZipInputStream(is); - - ZipEntry entry; - // Session s = HibHelper.getSessionFactory().openSession(); - - while ((entry = zis.getNextEntry()) != null) { - String name = entry.getName(); - // Only read files in the right path - if (name.startsWith("data-master/db/") && entry.getSize() != 0l && !name.contains(".gitignore")) { - byte[] file = new byte[(int) entry.getSize()]; - Scanner sc = new Scanner(zis); - String jsonStr = ""; - while (sc.hasNextLine()) { - jsonStr += sc.nextLine(); - } - // convert new line chars to HTML - jsonStr = jsonStr.replaceAll("(\\\\n)", "
"); - - // parse the json files. - JSONObject json = (JSONObject) new JSONParser().parse(jsonStr); - - List cats = (List) em.createQuery("from Category").getResultList(); - HashMap catmap = new HashMap(); - for (Category c : cats) { - catmap.put(c.getName(), c); - } - String title = (String) json.get("title"); - // Input validation findings - JSONArray tags = (JSONArray) json.get("tags"); - if (tags == null) - tags = new JSONArray(); - JSONObject fix = (JSONObject) json.get("fix"); - Object ref = json.get("references"); - - DefaultVulnerability dv = (DefaultVulnerability) em - .createQuery("from DefaultVulnerability where name = :name").setParameter("name", title) - .getResultList().stream().findFirst().orElse(null); - if (dv == null) - dv = new DefaultVulnerability(); - - dv.setName(title); - dv.setDescription( - getDescriptionFromVulnDB((String) (((JSONObject) json.get("description")).get("$ref")))); - - if (ref != null) { - String addRef = dv.getDescription() + "

References:
"; - if (ref.getClass().getName().contains("JSONArray")) { - JSONArray ja = (JSONArray) ref; - for (int i = 0; i < ja.size(); i++) { - String url = (String) ((JSONObject) ja.get(i)).get("url"); - String t = (String) ((JSONObject) ja.get(i)).get("title"); - addRef += "" + t + "
"; - } - } else { - String url = (String) ((JSONObject) ref).get("url"); - String t = (String) ((JSONObject) ref).get("title"); - addRef += "" + t + "
"; - } - dv.setDescription(addRef); - - } - dv.setRecommendation(getFixFromVulnDB((String) (((JSONObject) fix.get("guidance")).get("$ref")))); - - dv.setOverall(setSeverity((String) json.get("severity"))); - dv.setLikelyhood(setSeverity((String) json.get("severity"))); - dv.setImpact(setSeverity((String) json.get("severity"))); - - if (tags.contains("session") || tags.contains("authentication")) - dv.setCategory(catmap.get(ACCESS)); - else if (tags.contains("injection") || tags.contains("xss")) - dv.setCategory(catmap.get(INPUT)); - else if (tags.contains("csrf")) - dv.setCategory(catmap.get(ACCESS)); - else if (tags.contains("options")) - dv.setCategory(catmap.get(SERVER)); - else if (tags.contains("common")) - dv.setCategory(catmap.get(SERVER)); - else if (title.contains("cookie")) - dv.setCategory(catmap.get(SERVER)); - else if (title.contains("disclosure") || title.contains("disclosed")) - dv.setCategory(catmap.get(DATAEX)); - else if (tags.contains("path")) - dv.setCategory(catmap.get(SERVER)); - else if (tags.contains("upload")) - dv.setCategory(catmap.get(INPUT)); - else if (title.contains("header")) - dv.setCategory(catmap.get(SERVER)); - else if (tags.contains("server")) - dv.setCategory(catmap.get(SERVER)); - else if (title.contains("Insecure client-access policy")) - dv.setCategory(catmap.get(SERVER)); - else if (title.contains("Allow-Origin header")) - dv.setCategory(catmap.get(SERVER)); - else if (title.contains("Insecure cross-domain policy")) - dv.setCategory(catmap.get(SERVER)); - else if (tags.contains("resource")) - dv.setCategory(catmap.get(SERVER)); - else if (title.contains("auto-complete")) - dv.setCategory(catmap.get(DATAEX)); - else if (tags.contains("unencrypted") || tags.contains("ssl") || tags.contains("certificate") || tags.contains("cryptography")) - dv.setCategory(catmap.get(CRYPTO)); - else if (title.contains("XML External Entity")) - dv.setCategory(catmap.get(INPUT)); - else if (title.contains("configuration")) - dv.setCategory(catmap.get(SERVER)); - else if (tags.contains("bash")) - dv.setCategory(catmap.get(SERVER)); - else if (tags.contains("information leak")) - dv.setCategory(catmap.get(DATAEX)); - else if (tags.contains("credentials")) - dv.setCategory(catmap.get(ACCESS)); - else if (tags.contains("known cve")) - dv.setCategory(catmap.get(PUBLIC)); - else if (tags.contains("outdated components")) - dv.setCategory(catmap.get(OUTDATED)); - else - dv.setCategory(catmap.get(UNKNOWN)); - em.persist(dv); - /* - * s.getTransaction().begin(); s.save(dv); s.getTransaction().commit(); - */ - - } - - } - - } - - private static int setSeverity(String sev) { - if (sev.equals("critical")) - return 5; - else if (sev.equals("high")) - return 4; - else if (sev.equals("medium")) - return 3; - else if (sev.equals("low")) - return 2; - else if (sev.equals("informational")) - return 0; - else if (sev.equals("recommended")) - return 1; - else - return 0; - } - - private static String convertVDBText(Object text) { - if (text.getClass().getName().contains("String")) - return (String) text; - else { - String response = ""; - for (int i = 0; i < ((JSONArray) text).size(); i++) { - response += (String) ((JSONArray) text).get(i) + " "; - - } - return response; - } - } - - private static void createCategories(EntityManager em) { - // Session s = HibHelper.getSessionFactory().openSession(); - - List defaults = new ArrayList(); - defaults.add(INPUT); - defaults.add(SERVER); - defaults.add(CRYPTO); - defaults.add(DATAEX); - defaults.add(ACCESS); - defaults.add(PUBLIC); - defaults.add(UNKNOWN); - defaults.add(OUTDATED); - List cats = (List) em.createQuery("from Category").getResultList(); - - for (String name : defaults) { - boolean found = false; - for (Category c : cats) { - if (c.getName().equals(name)) { - found = true; - break; - } - } - if (!found) { - Category newCat = new Category(); - newCat.setName(name); - em.persist(newCat); - /* - * s.getTransaction().begin(); s.save(newCat); s.getTransaction().commit(); - */ - - } - - } - - } - - public static String decryptPassword(String password) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - String secret = System.getenv("FACTION_SECRET_KEY"); - byte[] hash = md.digest(secret.getBytes()); - char[] b64hash = Base64.encodeBase64String(hash).toCharArray(); - - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - KeySpec spec = new PBEKeySpec(b64hash, "f04ce910-bedb-4d8f-a023-4d2441dc0fba".getBytes(), 65536, 256); - SecretKey tmp = factory.generateSecret(spec); - SecretKey SecKey = new SecretKeySpec(tmp.getEncoded(), "AES"); - - Cipher AesCipher = Cipher.getInstance("AES"); - AesCipher.init(Cipher.DECRYPT_MODE, SecKey); - byte[] cypherText = Base64.decodeBase64(password); - byte[] bytePlainText = AesCipher.doFinal(cypherText); - return new String(bytePlainText); - - } catch (Exception ex) { - return ""; - } - - } - public static byte [] decryptBytes(String data) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - String secret = System.getenv("FACTION_SECRET_KEY"); - byte[] hash = md.digest(secret.getBytes()); - char[] b64hash = Base64.encodeBase64String(hash).toCharArray(); - - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - KeySpec spec = new PBEKeySpec(b64hash, "f04ce910-bedb-4d8f-a023-4d2441dc0fba".getBytes(), 65536, 256); - SecretKey tmp = factory.generateSecret(spec); - SecretKey SecKey = new SecretKeySpec(tmp.getEncoded(), "AES"); - - Cipher AesCipher = Cipher.getInstance("AES"); - AesCipher.init(Cipher.DECRYPT_MODE, SecKey); - byte[] cypherText = Base64.decodeBase64(data); - byte[] bytePlainText = AesCipher.doFinal(cypherText); - return bytePlainText; - - } catch (Exception ex) { - System.out.println(ex); - return null; - } - - } - - public static String md5hash(String data) { - try { - MessageDigest md; - md = MessageDigest.getInstance("md5"); - byte[] hash = md.digest(data.getBytes()); - return Hex.encodeHexString( hash ); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - return null; - } - } - public static String md5hash(byte [] data) { - try { - MessageDigest md; - md = MessageDigest.getInstance("md5"); - byte[] hash = md.digest(data); - return Hex.encodeHexString( hash ); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - return null; - } - } - - public static String encryptPassword(String password) { - try { - - MessageDigest md = MessageDigest.getInstance("SHA-256"); - String secret = System.getenv("FACTION_SECRET_KEY"); - byte[] hash = md.digest(secret.getBytes()); - char[] b64hash = Base64.encodeBase64String(hash).toCharArray(); - - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - KeySpec spec = new PBEKeySpec(b64hash, "f04ce910-bedb-4d8f-a023-4d2441dc0fba".getBytes(), 65536, 256); - SecretKey tmp = factory.generateSecret(spec); - SecretKey SecKey = new SecretKeySpec(tmp.getEncoded(), "AES"); - - Cipher AesCipher = Cipher.getInstance("AES"); - - byte[] byteText = password.getBytes(); - - AesCipher.init(Cipher.ENCRYPT_MODE, SecKey); - byte[] byteCipherText = AesCipher.doFinal(byteText); - - return Base64.encodeBase64String(byteCipherText); - - } catch (Exception Ex) { - Ex.printStackTrace(); - return null; - } - - } - public static String encryptBytes(byte [] data) { - try { - - MessageDigest md = MessageDigest.getInstance("SHA-256"); - String secret = System.getenv("FACTION_SECRET_KEY"); - byte[] hash = md.digest(secret.getBytes()); - char[] b64hash = Base64.encodeBase64String(hash).toCharArray(); - - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - KeySpec spec = new PBEKeySpec(b64hash, "f04ce910-bedb-4d8f-a023-4d2441dc0fba".getBytes(), 65536, 256); - SecretKey tmp = factory.generateSecret(spec); - SecretKey SecKey = new SecretKeySpec(tmp.getEncoded(), "AES"); - - Cipher AesCipher = Cipher.getInstance("AES"); - - - AesCipher.init(Cipher.ENCRYPT_MODE, SecKey); - byte[] byteCipherText = AesCipher.doFinal(data); - - return Base64.encodeBase64String(byteCipherText); - - } catch (Exception Ex) { - Ex.printStackTrace(); - return null; - } - - } - - - /** - * Creates ICS content for a calendar event - */ - public static String createICSContent(String title, String description, String location, - LocalDateTime startTime, LocalDateTime endTime, - String organizer, String[] attendees) { - - StringBuilder ics = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"); - - // Convert to UTC for ICS format - if(startTime == null) { - startTime = LocalDateTime.now(); - } - if(endTime == null) { - endTime = LocalDateTime.now(); - } - String startUTC = startTime.atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneId.of("UTC")) - .format(formatter); - String endUTC = endTime.atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneId.of("UTC")) - .format(formatter); - String nowUTC = LocalDateTime.now().atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneId.of("UTC")) - .format(formatter); - - // Generate unique ID - - String uid = UUID.randomUUID().toString() + "@factionsecurity.com"; - - // Build ICS content - ics.append("BEGIN:VCALENDAR\r\n"); - ics.append("VERSION:2.0\r\n"); - ics.append("PRODID:-//FACTIONSECURITYLLC//FACTION//EN\r\n"); - ics.append("METHOD:REQUEST\r\n"); - ics.append("CALSCALE:GREGORIAN\r\n"); - - ics.append("BEGIN:VEVENT\r\n"); - ics.append("UID:").append(uid).append("\r\n"); - ics.append("DTSTAMP:").append(nowUTC).append("\r\n"); - ics.append("DTSTART:").append(startUTC).append("\r\n"); - ics.append("DTEND:").append(endUTC).append("\r\n"); - ics.append("SUMMARY:").append(escapeText(title)).append("\r\n"); - ics.append("DESCRIPTION:").append(escapeText(description)).append("\r\n"); - - if (location != null && !location.trim().isEmpty()) { - ics.append("LOCATION:").append(escapeText(location)).append("\r\n"); - } - - ics.append("ORGANIZER:MAILTO:").append(organizer).append("\r\n"); - - // Add attendees - if (attendees != null) { - for (String attendee : attendees) { - ics.append("ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;") - .append("RSVP=TRUE:MAILTO:").append(attendee).append("\r\n"); - } - } - - ics.append("STATUS:CONFIRMED\r\n"); - ics.append("SEQUENCE:0\r\n"); - ics.append("REQUEST-STATUS:2.0;Success\r\n"); - ics.append("END:VEVENT\r\n"); - ics.append("END:VCALENDAR\r\n"); - - return ics.toString(); - } - - /** - * Escapes special characters in ICS text fields - */ - private static String escapeText(String text) { - if (text == null) return ""; - return text.replace("\\", "\\\\") - .replace(",", "\\,") - .replace(";", "\\;") - .replace("\n", "\\n") - .replace("\r", ""); - } - - - - private static String getDescriptionFromVulnDB(String reference) { - try { - String reference_id = reference.replace("#/files/description/", ""); - URL url = new URL("https://raw.githubusercontent.com/factionsecurity/data/master/db/en/description/" - + reference_id + ".md"); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - InputStream responseStream = connection.getInputStream(); - String contents = IOUtils.toString(responseStream, StandardCharsets.UTF_8); - contents = convertFromMarkDown(contents); - /// This line is because new lines show up string concatinated in the editor. - contents = contents.replaceAll("\n", " "); - return contents; - } catch (MalformedURLException e) { - e.printStackTrace(); - return ""; - } catch (IOException e) { - e.printStackTrace(); - return ""; - } - } - - private static String getFixFromVulnDB(String reference) { - try { - String reference_id = reference.replace("#/files/fix/", ""); - URL url; - url = new URL( - "https://raw.githubusercontent.com/factionsecurity/data/master/db/en/fix/" + reference_id + ".md"); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - InputStream responseStream = connection.getInputStream(); - String contents = IOUtils.toString(responseStream, StandardCharsets.UTF_8); - contents = convertFromMarkDown(contents); - return contents; - } catch (MalformedURLException e) { - e.printStackTrace(); - return ""; - } catch (IOException e) { - e.printStackTrace(); - return ""; - } - - } - - public static String getEnv(String ENV_VAR) { - String var = System.getenv(ENV_VAR); - // Fall back to JVM system properties so tests (and other callers) can - // inject values without needing real OS environment variables. - if (var == null) { - var = System.getProperty(ENV_VAR); - } - return var == null ? "" : var; - } - - public static String getVersion(ServletContext servletContext) { - InputStream inputStream = servletContext.getResourceAsStream("/META-INF/MANIFEST.MF"); - Manifest manifest; - try { - manifest = new Manifest(inputStream); - return "Version " + manifest.getMainAttributes().getValue("Implementation-Version"); - - } catch (IOException e) { - } catch(Exception e) { - } - return ""; - - } - - public static ReportOptions getOrCreateReportOptionsIfNotExist(EntityManager em) { - ReportOptions RPO = (ReportOptions) em.createQuery("from ReportOptions").getResultList().stream() - .findFirst().orElse(null); - - if (RPO == null) { - HibHelper.getInstance().preJoin(); - em.joinTransaction(); - RPO = new ReportOptions(); - RPO.setFont("Arial"); - RPO.setSize("12px"); - RPO.setBodyCss( - "body{ \r\n" + - " font-size: 15px; \r\n" + - "} \r\n" + - "figure{ \r\n" + - " text-align: center; \r\n" + - " padding: 0px; \r\n" + - " margin: 10px 0px; \r\n" + - " display: inline-block; \r\n" + - " border: none; \r\n" + - "} \r\n" + - "img{ \r\n" + - " max-width: 600px; \r\n" + - " height: auto !important; \r\n" + - " display: block; \r\n" + - " margin: auto !important\r\n" + - "} \r\n" + - "p{ \r\n" + - " padding:0px !important; \r\n" + - " margin:0px !important; \r\n" + - " margin-bottom: 0px !important; \r\n" + - "} \r\n" + - "li{ \r\n" + - " margin-bottom: 10px !important; \r\n" + - "} \r\n" + - "code { \r\n" + - " font-family: monospace!important; \r\n" + - " color: #666; \r\n" + - " background-color: #eeeeee !important; \r\n" + - " border-radius: 6px !important; \r\n" + - " padding-left: 100px !important; \r\n" + - "} \r\n" + - "code span{ \r\n" + - " font-family: monospace!important; \r\n" + - " color: #666; \r\n" + - " background-color: #eeeeee !important; \r\n" + - " border-radius: 6px !important; \r\n" + - "} \r\n" + - "table {\r\n" + - " font-family: Arial, Helvetica, sans-serif;\r\n" + - " border-collapse: collapse;\r\n" + - " width: 100%;\r\n" + - " max-width: 480px;\r\n" + - "}\r\n" + - "td, th {\r\n" + - " border: 0.3px solid #acb9ca;\r\n" + - " padding-left: 8px;\r\n" + - "}\r\n" + - "td div {\r\n" + - " word-break: break-all !important;\r\n" + - "}\r\n" + - "th {\r\n" + - " white-space: nowrap !important;\r\n" + - " background-color: #afbfcf;\r\n" + - " font-weight: normal;\r\n" + - "}\r\n" + - "pre{ \r\n" + - " background-color:#eeeeee !important; \r\n" + - " border:1px solid #cccccc !important; \r\n" + - " font-size:15px; \r\n" + - " padding: 10px 15px; \r\n" + - "}\r\n" - ); - - em.persist(RPO); - HibHelper.getInstance().commit(); - } - return RPO; - } - - public static String convertFromMarkDown(String text) { - try { - List extensions = Arrays.asList(TablesExtension.create()); - Parser parser = Parser.builder() - .extensions(extensions) - .build(); - Node document = parser.parse(text); - HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).build(); - String converted = renderer.render(document); - converted = converted.replaceAll("\\+\\+([^+]+)\\+\\+", "$1"); // Allow for custom underline markdown - converted += "
"; - converted = converted.replaceAll("
", "\r\n").replaceAll("
","\r\n"); - converted = converted.replaceAll("

\\s*

", "


"); - return converted; - } catch (Exception ex) { - ex.printStackTrace(); - return text; - } - } - public static Date getDue(EntityManager em, Date start, int Level){ - RiskLevel level = (RiskLevel)em.createQuery("from RiskLevel where riskId = :id") - .setParameter("id", Level).getResultList() - .stream().findFirst().orElse(null); - if(level.getDaysTillDue() == null) - return null; - Calendar dueDate = Calendar.getInstance(); - dueDate.setTime(start); - dueDate.add(Calendar.DAY_OF_YEAR, level.getDaysTillDue()); - return dueDate.getTime(); - } - - public static Date getWarn(Date end,int days){ - Calendar dueDate = Calendar.getInstance(); - dueDate.setTime(end); - dueDate.add(Calendar.DAY_OF_YEAR, - days); - return dueDate.getTime(); - } - - public static Date getWarning(EntityManager em, Date start, int Level){ - RiskLevel level = (RiskLevel)em.createQuery("from RiskLevel where riskId = :id") - .setParameter("id", Level).getResultList() - .stream().findFirst().orElse(null); - if(level.getDaysTillWarning() == null) - return null; - Calendar dueDate = Calendar.getInstance(); - dueDate.setTime(start); - dueDate.add(Calendar.DAY_OF_YEAR, level.getDaysTillWarning()); - return dueDate.getTime(); - } - - public static String addBadge(String title, String color, String icon) { - return String.format("%s", - color, - icon, - title); - } - - public static void CheckForUpdatedCustomFields(Assessment assessment, EntityManager em) { - - if(assessment.isFinalized()) { - return; - } - List types = em - .createQuery("from CustomType where type = :variableType and (deleted IS NULL or deleted = false)") - .setParameter("variableType", CustomType.ObjType.ASMT.getValue()) - .getResultList(); - - - if(CustomType.FieldType.values().length > 3) { - types = types - .stream() - .filter( vType -> - vType.getAssessmentTypes() - .stream() - .anyMatch( aType -> - aType.getId().equals(assessment.getType().getId()) - )) - .collect(Collectors.toList()); - } - final List fields = assessment.getCustomFields(); - - final List filteredTypes = types; - - // Identify CustomFields that should be removed (no longer enabled for the assessment type) - List fieldsToRemove = fields.stream() - .filter( f -> !filteredTypes.stream().anyMatch(t -> t.equals( f.getType() ))) - .collect(Collectors.toList()); - - // Find New Fields we need to add - List newTypes = filteredTypes.stream() - .filter( t -> !fields.stream().anyMatch(f -> f.getType().equals(t) ) ) - .collect(Collectors.toList()); - - System.out.println("CustomFields to remove: " + fieldsToRemove.size()); - System.out.println("Found new CustomTypes to add : " + newTypes.size() ); - - // Just return if there are no changes - if(fieldsToRemove.isEmpty() && newTypes.isEmpty()) { - System.out.println("No Custom Field Changes Found"); - return; - } - - HibHelper.getInstance().preJoin(); - em.joinTransaction(); - - // Remove fields that are no longer applicable - modify the existing collection in-place - fields.removeAll(fieldsToRemove); - - // Create and add new fields directly to the existing collection - for(CustomType type : newTypes) { - CustomField newField = new CustomField(); - newField.setType(type); - newField.setValue(type.getDefaultValue()); - em.persist(newField); - fields.add(newField); - } - - // Persist the assessment - the collection is already modified in-place - em.persist(assessment); - HibHelper.getInstance().commit(); - - System.out.println("CustomFields Updated to " + fields.size() + " fields"); - } - -} +package com.fuse.utils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.Authenticator; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.PasswordAuthentication; +import java.net.Proxy; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.KeySpec; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Scanner; +import java.util.TreeMap; +import java.util.UUID; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import org.apache.struts2.ServletActionContext; +import javax.persistence.EntityManager; +import javax.servlet.ServletContext; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.IOUtils; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.owasp.html.HtmlPolicyBuilder; +import org.owasp.html.PolicyFactory; +import org.w3c.tidy.Tidy; + +import com.fuse.dao.Assessment; +import com.fuse.dao.Category; +import com.fuse.dao.CustomField; +import com.fuse.dao.CustomType; +import com.fuse.dao.DefaultVulnerability; +import com.fuse.dao.HibHelper; +import com.fuse.dao.ReportOptions; +import com.fuse.dao.RiskLevel; +import com.fuse.dao.Vulnerability; +import com.fuse.dao.CustomType; + +import org.commonmark.Extension; +import org.commonmark.ext.autolink.AutolinkExtension; +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.ext.ins.InsExtension; +import org.commonmark.node.*; +import org.commonmark.parser.IncludeSourceSpans; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + +public class FSUtils { + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + public static String generatePasswd() { + String chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(32); + for (int i = 0; i < 32; i++) { + sb.append(chars.charAt(SECURE_RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + private static String INPUT = "Unvalidated Input"; + private static String SERVER = "Server Misconfiguration"; + private static String CRYPTO = "Weak Cryptography"; + private static String DATAEX = "Data Exposure"; + private static String ACCESS = "Broken Access Control and Session Management"; + private static String PUBLIC = "Publicly Known Vulnerability"; + private static String OUTDATED = "Outdated Libraries and Components"; + private static String UNKNOWN = "Uncategorized"; + + public static String jtidy(String html) { + + MethodProfiler.ProfileContext context = MethodProfiler.start("FSUtils", "jtidy"); + try { + // figures seems to kill the whole message. + //html = html.replaceAll("<(/)?figure>", ""); + if(!html.contains("<")) { + return html; + } + + Tidy tidy = new Tidy(); + InputStream stream = new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8)); + // tidy.setXmlOut(true); + tidy.setQuiet(true); + tidy.setWord2000(false); + tidy.setQuoteAmpersand(true); + tidy.setQuoteMarks(true); + tidy.setQuoteNbsp(true); + tidy.setTidyMark(false); + tidy.setShowErrors(0); + tidy.setShowWarnings(false); + tidy.setWraplen(0); + tidy.setWrapAttVals(false); + tidy.setPrintBodyOnly(true); + tidy.setXHTML(true); + + tidy.setOutputEncoding("UTF-8"); + tidy.setInputEncoding("UTF-8"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + tidy.parse(stream, baos); + try { + String out = new String(baos.toByteArray(), "UTF-8"); + out = out.replaceAll(" ", " "); + ArrayList updated = new ArrayList(); + Boolean preSection=false; + for (String line : out.split("\n")) { + //line = line.replaceAll("^[ ]+", ""); + if(line.contains("")) { + preSection = true; + } + if (line.contains("")) { + preSection = false; + } + if(preSection) { + line = line + "\n"; + } + updated.add(line); + } + out = String.join("", updated); + + return out; + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return new String(baos.toByteArray()); + } + }finally { + context.end(); + } + } + + public static String sanitizeHTML(String html) { + PolicyFactory policyBuilder = new HtmlPolicyBuilder().allowAttributes("src").onElements("img") + .allowUrlProtocols("data", "http", "https").allowAttributes("href").onElements("a") + .allowAttributes("src", "width", "height", "controls").onElements("video") + .allowAttributes("style", "class", "colspan").onElements("a", "label", "h1", "h2", "h3", "h4", "h5", "h6", "p", "i", "b", "u", "strong", "em", + "small", "big", "pre", "code", "cite", "samp", "sub", "sup", "strike", "center", "blockquote", + "hr", "br", "col", "font", "div", "img", "ul", "ol", "li", "dd", "dt", "dl", "tbody", "thead", + "tfoot", "table", "td", "th", "tr", "colgroup", "fieldset", "legend", "span", "backquote") + .allowAttributes("data-changedata", "data-cid", "data-last-change-time", "data-time", "data-userid", + "data-username", "title").onElements("span") + .allowAttributes("border", "cellpadding", "cellspacing", "style", "class", "colspan").onElements("table") + .allowStandardUrlProtocols() + .allowElements("a", "label", "h1", "h2", "h3", "h4", "h5", "h6", "p", "i", "b", "u", "strong", "em", + "small", "big", "pre", "code", "cite", "samp", "sub", "sup", "strike", "center", "blockquote", + "hr", "br", "col", "font", "div", "img", "ul", "ol", "li", "dd", "dt", "dl", "tbody", "thead", + "tfoot", "table", "td", "th", "tr", "colgroup", "fieldset", "legend", "del", "ins", "figure", + "span", "figcaption", "ins") + .toFactory(); + String sanitized = policyBuilder.sanitize(html); + + /// Adding regex for MS Word table copy and paste that mess everything up. + return sanitized.replaceAll("(style=\".*? )(windowtext)", "$1").replaceAll("style=\"line-height:normal\"", ""); + + } + + /** + * True when a value contains characters that could open an HTML tag. Used to + * reject plain-text settings (titles, type names) that are later shown in the + * UI. Output encoding is still applied at render time; this is defence in depth. + */ + /** + * Returns the uploaded file after checking it really is a multipart temp file that + * Struts wrote under the servlet temp directory (or java.io.tmpdir when no servlet + * context is available, e.g. in unit tests). Struts already decides where uploads + * land, so this is defence in depth: it pins the path we read to the upload area + * and makes that visible to static analysis. + */ + public static File checkUploadedFile(File upload) throws IOException { + if (upload == null) { + throw new IOException("No file uploaded"); + } + Path real = upload.toPath().toRealPath(); + for (Path base : uploadDirectories()) { + if (real.startsWith(base)) { + return real.toFile(); + } + } + throw new IOException("Uploaded file is outside the upload directory"); + } + + private static List uploadDirectories() { + List dirs = new ArrayList<>(); + try { + ServletContext ctx = ServletActionContext.getServletContext(); + Object tmp = ctx == null ? null : ctx.getAttribute("javax.servlet.context.tempdir"); + if (tmp instanceof File) { + dirs.add(((File) tmp).toPath().toRealPath()); + } + } catch (Exception ignore) { + // no action context (unit tests, background threads) + } + try { + dirs.add(Paths.get(System.getProperty("java.io.tmpdir")).toRealPath()); + } catch (Exception ignore) { + } + return dirs; + } + + public static boolean containsHTML(String value) { + return value != null && (value.indexOf('<') >= 0 || value.indexOf('>') >= 0); + } + + public static String sanitizeGUID(String guid) { + String regexGUID = "^[0-9a-zA-Z\\-].*$"; + Matcher guids = Pattern.compile(regexGUID).matcher(guid); + if (guids.find()) + return guids.group(); + else + return ""; + + } + + public static String sanitizeMongo(String input) { + if (input == null) return ""; + return input.replaceAll("([+?*^${}()|\\[\\]\\\\])", "\\\\$1"); + } + + public static boolean checkEmail(String email) { + String emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"; + Matcher match = Pattern.compile(emailRegex).matcher(email); + return match.matches(); + + } + + public static List sortVulns(List vulns) { + if (vulns == null) + return new ArrayList(); + + TreeMap sorted = new TreeMap(); + for (Vulnerability v : vulns) { + sorted.put(v.getOverall(), v); + } + return new ArrayList(sorted.values()); + + } + + public static List sortUniqueAssessment(List asmts) { + if (asmts == null) + return new ArrayList(); + TreeMap sorted = new TreeMap(); + for (Assessment a : asmts) { + sorted.put(a.getAppId(), a); + } + return new ArrayList(sorted.values()); + + } + + public static void importVulnDB(String proxyurl, int port, String username, String password, EntityManager em) + throws IOException, ParseException { + + // Create the default Categories + createCategories(em); + // Load external resource of vulnerability data. + String vdbzip = "https://codeload.github.com/factionsecurity/data/zip/master"; + URL obj = new URL(vdbzip); + HttpURLConnection conn = null; + if (proxyurl != null) { + + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyurl, port)); + if (username != null) { + Authenticator authenticator = new Authenticator() { + + public PasswordAuthentication getPasswordAuthentication() { + return (new PasswordAuthentication("user", "password".toCharArray())); + } + }; + Authenticator.setDefault(authenticator); + } + conn = (HttpURLConnection) obj.openConnection(proxy); + } else { + + conn = (HttpURLConnection) obj.openConnection(); + } + + InputStream is = conn.getInputStream(); + + // Unzip the files into memory + ZipInputStream zis = new ZipInputStream(is); + + ZipEntry entry; + // Session s = HibHelper.getSessionFactory().openSession(); + + while ((entry = zis.getNextEntry()) != null) { + String name = entry.getName(); + // Only read files in the right path + if (name.startsWith("data-master/db/") && entry.getSize() != 0l && !name.contains(".gitignore")) { + byte[] file = new byte[(int) entry.getSize()]; + Scanner sc = new Scanner(zis); + String jsonStr = ""; + while (sc.hasNextLine()) { + jsonStr += sc.nextLine(); + } + // convert new line chars to HTML + jsonStr = jsonStr.replaceAll("(\\\\n)", "
"); + + // parse the json files. + JSONObject json = (JSONObject) new JSONParser().parse(jsonStr); + + List cats = (List) em.createQuery("from Category").getResultList(); + HashMap catmap = new HashMap(); + for (Category c : cats) { + catmap.put(c.getName(), c); + } + String title = (String) json.get("title"); + // Input validation findings + JSONArray tags = (JSONArray) json.get("tags"); + if (tags == null) + tags = new JSONArray(); + JSONObject fix = (JSONObject) json.get("fix"); + Object ref = json.get("references"); + + DefaultVulnerability dv = (DefaultVulnerability) em + .createQuery("from DefaultVulnerability where name = :name").setParameter("name", title) + .getResultList().stream().findFirst().orElse(null); + if (dv == null) + dv = new DefaultVulnerability(); + + dv.setName(title); + dv.setDescription( + getDescriptionFromVulnDB((String) (((JSONObject) json.get("description")).get("$ref")))); + + if (ref != null) { + String addRef = dv.getDescription() + "

References:
"; + if (ref.getClass().getName().contains("JSONArray")) { + JSONArray ja = (JSONArray) ref; + for (int i = 0; i < ja.size(); i++) { + String url = (String) ((JSONObject) ja.get(i)).get("url"); + String t = (String) ((JSONObject) ja.get(i)).get("title"); + addRef += "" + t + "
"; + } + } else { + String url = (String) ((JSONObject) ref).get("url"); + String t = (String) ((JSONObject) ref).get("title"); + addRef += "" + t + "
"; + } + dv.setDescription(addRef); + + } + dv.setRecommendation(getFixFromVulnDB((String) (((JSONObject) fix.get("guidance")).get("$ref")))); + + dv.setOverall(setSeverity((String) json.get("severity"))); + dv.setLikelyhood(setSeverity((String) json.get("severity"))); + dv.setImpact(setSeverity((String) json.get("severity"))); + + if (tags.contains("session") || tags.contains("authentication")) + dv.setCategory(catmap.get(ACCESS)); + else if (tags.contains("injection") || tags.contains("xss")) + dv.setCategory(catmap.get(INPUT)); + else if (tags.contains("csrf")) + dv.setCategory(catmap.get(ACCESS)); + else if (tags.contains("options")) + dv.setCategory(catmap.get(SERVER)); + else if (tags.contains("common")) + dv.setCategory(catmap.get(SERVER)); + else if (title.contains("cookie")) + dv.setCategory(catmap.get(SERVER)); + else if (title.contains("disclosure") || title.contains("disclosed")) + dv.setCategory(catmap.get(DATAEX)); + else if (tags.contains("path")) + dv.setCategory(catmap.get(SERVER)); + else if (tags.contains("upload")) + dv.setCategory(catmap.get(INPUT)); + else if (title.contains("header")) + dv.setCategory(catmap.get(SERVER)); + else if (tags.contains("server")) + dv.setCategory(catmap.get(SERVER)); + else if (title.contains("Insecure client-access policy")) + dv.setCategory(catmap.get(SERVER)); + else if (title.contains("Allow-Origin header")) + dv.setCategory(catmap.get(SERVER)); + else if (title.contains("Insecure cross-domain policy")) + dv.setCategory(catmap.get(SERVER)); + else if (tags.contains("resource")) + dv.setCategory(catmap.get(SERVER)); + else if (title.contains("auto-complete")) + dv.setCategory(catmap.get(DATAEX)); + else if (tags.contains("unencrypted") || tags.contains("ssl") || tags.contains("certificate") || tags.contains("cryptography")) + dv.setCategory(catmap.get(CRYPTO)); + else if (title.contains("XML External Entity")) + dv.setCategory(catmap.get(INPUT)); + else if (title.contains("configuration")) + dv.setCategory(catmap.get(SERVER)); + else if (tags.contains("bash")) + dv.setCategory(catmap.get(SERVER)); + else if (tags.contains("information leak")) + dv.setCategory(catmap.get(DATAEX)); + else if (tags.contains("credentials")) + dv.setCategory(catmap.get(ACCESS)); + else if (tags.contains("known cve")) + dv.setCategory(catmap.get(PUBLIC)); + else if (tags.contains("outdated components")) + dv.setCategory(catmap.get(OUTDATED)); + else + dv.setCategory(catmap.get(UNKNOWN)); + em.persist(dv); + /* + * s.getTransaction().begin(); s.save(dv); s.getTransaction().commit(); + */ + + } + + } + + } + + private static int setSeverity(String sev) { + if (sev.equals("critical")) + return 5; + else if (sev.equals("high")) + return 4; + else if (sev.equals("medium")) + return 3; + else if (sev.equals("low")) + return 2; + else if (sev.equals("informational")) + return 0; + else if (sev.equals("recommended")) + return 1; + else + return 0; + } + + private static String convertVDBText(Object text) { + if (text.getClass().getName().contains("String")) + return (String) text; + else { + String response = ""; + for (int i = 0; i < ((JSONArray) text).size(); i++) { + response += (String) ((JSONArray) text).get(i) + " "; + + } + return response; + } + } + + private static void createCategories(EntityManager em) { + // Session s = HibHelper.getSessionFactory().openSession(); + + List defaults = new ArrayList(); + defaults.add(INPUT); + defaults.add(SERVER); + defaults.add(CRYPTO); + defaults.add(DATAEX); + defaults.add(ACCESS); + defaults.add(PUBLIC); + defaults.add(UNKNOWN); + defaults.add(OUTDATED); + List cats = (List) em.createQuery("from Category").getResultList(); + + for (String name : defaults) { + boolean found = false; + for (Category c : cats) { + if (c.getName().equals(name)) { + found = true; + break; + } + } + if (!found) { + Category newCat = new Category(); + newCat.setName(name); + em.persist(newCat); + /* + * s.getTransaction().begin(); s.save(newCat); s.getTransaction().commit(); + */ + + } + + } + + } + + // --------------------------------------------------------------------- + // Symmetric encryption of stored secrets (SMTP/LDAP passwords, API keys, + // keystores, report passwords). The key is derived from FACTION_SECRET_KEY. + // + // Current format ("v2"): $AESGCM$ + base64( salt[16] || iv[12] || AES-GCM ciphertext+tag ) + // - a fresh random salt and IV are generated for every value, so identical + // plaintexts never produce identical ciphertexts and the PBKDF2 salt is + // never reused. + // Legacy format (pre 1.8.14): base64( AES/ECB ciphertext ) with a constant + // PBKDF2 salt. It is still *read* so that values already in the database + // keep working; it is never written. Values are re-encrypted in the new + // format the next time they are saved. + // --------------------------------------------------------------------- + private static final String GCM_PREFIX = "$AESGCM$"; + private static final int GCM_SALT_LENGTH = 16; + private static final int GCM_IV_LENGTH = 12; + private static final int GCM_TAG_BITS = 128; + private static final int PBKDF2_ITERATIONS = 65536; + private static final int AES_KEY_BITS = 256; + private static final byte[] LEGACY_SALT = "f04ce910-bedb-4d8f-a023-4d2441dc0fba".getBytes(); + + private static SecretKey deriveKey(byte[] salt) throws GeneralSecurityException { + String secret = getEnv("FACTION_SECRET_KEY"); + if (secret == null || secret.isEmpty()) { + throw new IllegalStateException("FACTION_SECRET_KEY is not set"); + } + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(secret.getBytes(StandardCharsets.UTF_8)); + char[] b64hash = Base64.encodeBase64String(hash).toCharArray(); + + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(b64hash, salt, PBKDF2_ITERATIONS, AES_KEY_BITS); + SecretKey tmp = factory.generateSecret(spec); + return new SecretKeySpec(tmp.getEncoded(), "AES"); + } + + private static String encryptToString(byte[] plaintext) throws GeneralSecurityException { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[GCM_SALT_LENGTH]; + byte[] iv = new byte[GCM_IV_LENGTH]; + random.nextBytes(salt); + random.nextBytes(iv); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, deriveKey(salt), new GCMParameterSpec(GCM_TAG_BITS, iv)); + byte[] ciphertext = cipher.doFinal(plaintext); + + byte[] out = new byte[GCM_SALT_LENGTH + GCM_IV_LENGTH + ciphertext.length]; + System.arraycopy(salt, 0, out, 0, GCM_SALT_LENGTH); + System.arraycopy(iv, 0, out, GCM_SALT_LENGTH, GCM_IV_LENGTH); + System.arraycopy(ciphertext, 0, out, GCM_SALT_LENGTH + GCM_IV_LENGTH, ciphertext.length); + return GCM_PREFIX + Base64.encodeBase64String(out); + } + + private static byte[] decryptToBytes(String data) throws GeneralSecurityException { + if (data == null) { + throw new IllegalArgumentException("nothing to decrypt"); + } + if (data.startsWith(GCM_PREFIX)) { + byte[] raw = Base64.decodeBase64(data.substring(GCM_PREFIX.length())); + int headerLength = GCM_SALT_LENGTH + GCM_IV_LENGTH; + if (raw.length < headerLength + (GCM_TAG_BITS / 8)) { + throw new GeneralSecurityException("ciphertext too short"); + } + byte[] salt = Arrays.copyOfRange(raw, 0, GCM_SALT_LENGTH); + byte[] iv = Arrays.copyOfRange(raw, GCM_SALT_LENGTH, headerLength); + byte[] ciphertext = Arrays.copyOfRange(raw, headerLength, raw.length); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, deriveKey(salt), new GCMParameterSpec(GCM_TAG_BITS, iv)); + return cipher.doFinal(ciphertext); + } + // Legacy AES/ECB value written by an earlier release. Read-only. + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, deriveKey(LEGACY_SALT)); + return cipher.doFinal(Base64.decodeBase64(data)); + } + + public static String decryptPassword(String password) { + try { + return new String(decryptToBytes(password), StandardCharsets.UTF_8); + } catch (Exception ex) { + return ""; + } + } + + public static byte[] decryptBytes(String data) { + try { + return decryptToBytes(data); + } catch (Exception ex) { + System.out.println(ex); + return null; + } + } + + public static String md5hash(String data) { + try { + MessageDigest md; + md = MessageDigest.getInstance("md5"); + byte[] hash = md.digest(data.getBytes()); + return Hex.encodeHexString( hash ); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + public static String md5hash(byte [] data) { + try { + MessageDigest md; + md = MessageDigest.getInstance("md5"); + byte[] hash = md.digest(data); + return Hex.encodeHexString( hash ); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + public static String encryptPassword(String password) { + try { + return encryptToString(password.getBytes(StandardCharsets.UTF_8)); + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + + public static String encryptBytes(byte[] data) { + try { + return encryptToString(data); + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + + + /** + * Creates ICS content for a calendar event + */ + public static String createICSContent(String title, String description, String location, + LocalDateTime startTime, LocalDateTime endTime, + String organizer, String[] attendees) { + + StringBuilder ics = new StringBuilder(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"); + + // Convert to UTC for ICS format + if(startTime == null) { + startTime = LocalDateTime.now(); + } + if(endTime == null) { + endTime = LocalDateTime.now(); + } + String startUTC = startTime.atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneId.of("UTC")) + .format(formatter); + String endUTC = endTime.atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneId.of("UTC")) + .format(formatter); + String nowUTC = LocalDateTime.now().atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneId.of("UTC")) + .format(formatter); + + // Generate unique ID + + String uid = UUID.randomUUID().toString() + "@factionsecurity.com"; + + // Build ICS content + ics.append("BEGIN:VCALENDAR\r\n"); + ics.append("VERSION:2.0\r\n"); + ics.append("PRODID:-//FACTIONSECURITYLLC//FACTION//EN\r\n"); + ics.append("METHOD:REQUEST\r\n"); + ics.append("CALSCALE:GREGORIAN\r\n"); + + ics.append("BEGIN:VEVENT\r\n"); + ics.append("UID:").append(uid).append("\r\n"); + ics.append("DTSTAMP:").append(nowUTC).append("\r\n"); + ics.append("DTSTART:").append(startUTC).append("\r\n"); + ics.append("DTEND:").append(endUTC).append("\r\n"); + ics.append("SUMMARY:").append(escapeText(title)).append("\r\n"); + ics.append("DESCRIPTION:").append(escapeText(description)).append("\r\n"); + + if (location != null && !location.trim().isEmpty()) { + ics.append("LOCATION:").append(escapeText(location)).append("\r\n"); + } + + ics.append("ORGANIZER:MAILTO:").append(organizer).append("\r\n"); + + // Add attendees + if (attendees != null) { + for (String attendee : attendees) { + ics.append("ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;") + .append("RSVP=TRUE:MAILTO:").append(attendee).append("\r\n"); + } + } + + ics.append("STATUS:CONFIRMED\r\n"); + ics.append("SEQUENCE:0\r\n"); + ics.append("REQUEST-STATUS:2.0;Success\r\n"); + ics.append("END:VEVENT\r\n"); + ics.append("END:VCALENDAR\r\n"); + + return ics.toString(); + } + + /** + * Escapes special characters in ICS text fields + */ + private static String escapeText(String text) { + if (text == null) return ""; + return text.replace("\\", "\\\\") + .replace(",", "\\,") + .replace(";", "\\;") + .replace("\n", "\\n") + .replace("\r", ""); + } + + + + private static String getDescriptionFromVulnDB(String reference) { + try { + String reference_id = reference.replace("#/files/description/", ""); + URL url = new URL("https://raw.githubusercontent.com/factionsecurity/data/master/db/en/description/" + + reference_id + ".md"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + InputStream responseStream = connection.getInputStream(); + String contents = IOUtils.toString(responseStream, StandardCharsets.UTF_8); + contents = convertFromMarkDown(contents); + /// This line is because new lines show up string concatinated in the editor. + contents = contents.replaceAll("\n", " "); + return contents; + } catch (MalformedURLException e) { + e.printStackTrace(); + return ""; + } catch (IOException e) { + e.printStackTrace(); + return ""; + } + } + + private static String getFixFromVulnDB(String reference) { + try { + String reference_id = reference.replace("#/files/fix/", ""); + URL url; + url = new URL( + "https://raw.githubusercontent.com/factionsecurity/data/master/db/en/fix/" + reference_id + ".md"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + InputStream responseStream = connection.getInputStream(); + String contents = IOUtils.toString(responseStream, StandardCharsets.UTF_8); + contents = convertFromMarkDown(contents); + return contents; + } catch (MalformedURLException e) { + e.printStackTrace(); + return ""; + } catch (IOException e) { + e.printStackTrace(); + return ""; + } + + } + + public static String getEnv(String ENV_VAR) { + String var = System.getenv(ENV_VAR); + // Fall back to JVM system properties so tests (and other callers) can + // inject values without needing real OS environment variables. + if (var == null) { + var = System.getProperty(ENV_VAR); + } + return var == null ? "" : var; + } + + public static String getVersion(ServletContext servletContext) { + InputStream inputStream = servletContext.getResourceAsStream("/META-INF/MANIFEST.MF"); + Manifest manifest; + try { + manifest = new Manifest(inputStream); + return "Version " + manifest.getMainAttributes().getValue("Implementation-Version"); + + } catch (IOException e) { + } catch(Exception e) { + } + return ""; + + } + + public static ReportOptions getOrCreateReportOptionsIfNotExist(EntityManager em) { + ReportOptions RPO = (ReportOptions) em.createQuery("from ReportOptions").getResultList().stream() + .findFirst().orElse(null); + + if (RPO == null) { + HibHelper.getInstance().preJoin(); + em.joinTransaction(); + RPO = new ReportOptions(); + RPO.setFont("Arial"); + RPO.setSize("12px"); + RPO.setBodyCss( + "body{ \r\n" + + " font-size: 15px; \r\n" + + "} \r\n" + + "figure{ \r\n" + + " text-align: center; \r\n" + + " padding: 0px; \r\n" + + " margin: 10px 0px; \r\n" + + " display: inline-block; \r\n" + + " border: none; \r\n" + + "} \r\n" + + "img{ \r\n" + + " max-width: 600px; \r\n" + + " height: auto !important; \r\n" + + " display: block; \r\n" + + " margin: auto !important\r\n" + + "} \r\n" + + "p{ \r\n" + + " padding:0px !important; \r\n" + + " margin:0px !important; \r\n" + + " margin-bottom: 0px !important; \r\n" + + "} \r\n" + + "li{ \r\n" + + " margin-bottom: 10px !important; \r\n" + + "} \r\n" + + "code { \r\n" + + " font-family: monospace!important; \r\n" + + " color: #666; \r\n" + + " background-color: #eeeeee !important; \r\n" + + " border-radius: 6px !important; \r\n" + + " padding-left: 100px !important; \r\n" + + "} \r\n" + + "code span{ \r\n" + + " font-family: monospace!important; \r\n" + + " color: #666; \r\n" + + " background-color: #eeeeee !important; \r\n" + + " border-radius: 6px !important; \r\n" + + "} \r\n" + + "table {\r\n" + + " font-family: Arial, Helvetica, sans-serif;\r\n" + + " border-collapse: collapse;\r\n" + + " width: 100%;\r\n" + + " max-width: 480px;\r\n" + + "}\r\n" + + "td, th {\r\n" + + " border: 0.3px solid #acb9ca;\r\n" + + " padding-left: 8px;\r\n" + + "}\r\n" + + "td div {\r\n" + + " word-break: break-all !important;\r\n" + + "}\r\n" + + "th {\r\n" + + " white-space: nowrap !important;\r\n" + + " background-color: #afbfcf;\r\n" + + " font-weight: normal;\r\n" + + "}\r\n" + + "pre{ \r\n" + + " background-color:#eeeeee !important; \r\n" + + " border:1px solid #cccccc !important; \r\n" + + " font-size:15px; \r\n" + + " padding: 10px 15px; \r\n" + + "}\r\n" + ); + + em.persist(RPO); + HibHelper.getInstance().commit(); + } + return RPO; + } + + public static String convertFromMarkDown(String text) { + try { + List extensions = Arrays.asList(TablesExtension.create()); + Parser parser = Parser.builder() + .extensions(extensions) + .build(); + Node document = parser.parse(text); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).build(); + String converted = renderer.render(document); + converted = converted.replaceAll("\\+\\+([^+]+)\\+\\+", "$1"); // Allow for custom underline markdown + converted += "
"; + converted = converted.replaceAll("
", "\r\n").replaceAll("
","\r\n"); + converted = converted.replaceAll("

\\s*

", "


"); + return converted; + } catch (Exception ex) { + ex.printStackTrace(); + return text; + } + } + public static Date getDue(EntityManager em, Date start, int Level){ + RiskLevel level = (RiskLevel)em.createQuery("from RiskLevel where riskId = :id") + .setParameter("id", Level).getResultList() + .stream().findFirst().orElse(null); + if(level.getDaysTillDue() == null) + return null; + Calendar dueDate = Calendar.getInstance(); + dueDate.setTime(start); + dueDate.add(Calendar.DAY_OF_YEAR, level.getDaysTillDue()); + return dueDate.getTime(); + } + + public static Date getWarn(Date end,int days){ + Calendar dueDate = Calendar.getInstance(); + dueDate.setTime(end); + dueDate.add(Calendar.DAY_OF_YEAR, - days); + return dueDate.getTime(); + } + + public static Date getWarning(EntityManager em, Date start, int Level){ + RiskLevel level = (RiskLevel)em.createQuery("from RiskLevel where riskId = :id") + .setParameter("id", Level).getResultList() + .stream().findFirst().orElse(null); + if(level.getDaysTillWarning() == null) + return null; + Calendar dueDate = Calendar.getInstance(); + dueDate.setTime(start); + dueDate.add(Calendar.DAY_OF_YEAR, level.getDaysTillWarning()); + return dueDate.getTime(); + } + + public static String addBadge(String title, String color, String icon) { + return String.format("%s", + color, + icon, + title); + } + + public static void CheckForUpdatedCustomFields(Assessment assessment, EntityManager em) { + + if(assessment.isFinalized()) { + return; + } + List types = em + .createQuery("from CustomType where type = :variableType and (deleted IS NULL or deleted = false)") + .setParameter("variableType", CustomType.ObjType.ASMT.getValue()) + .getResultList(); + + + if(CustomType.FieldType.values().length > 3) { + types = types + .stream() + .filter( vType -> + vType.getAssessmentTypes() + .stream() + .anyMatch( aType -> + aType.getId().equals(assessment.getType().getId()) + )) + .collect(Collectors.toList()); + } + final List fields = assessment.getCustomFields(); + + final List filteredTypes = types; + + // Identify CustomFields that should be removed (no longer enabled for the assessment type) + List fieldsToRemove = fields.stream() + .filter( f -> !filteredTypes.stream().anyMatch(t -> t.equals( f.getType() ))) + .collect(Collectors.toList()); + + // Find New Fields we need to add + List newTypes = filteredTypes.stream() + .filter( t -> !fields.stream().anyMatch(f -> f.getType().equals(t) ) ) + .collect(Collectors.toList()); + + System.out.println("CustomFields to remove: " + fieldsToRemove.size()); + System.out.println("Found new CustomTypes to add : " + newTypes.size() ); + + // Just return if there are no changes + if(fieldsToRemove.isEmpty() && newTypes.isEmpty()) { + System.out.println("No Custom Field Changes Found"); + return; + } + + HibHelper.getInstance().preJoin(); + em.joinTransaction(); + + // Remove fields that are no longer applicable - modify the existing collection in-place + fields.removeAll(fieldsToRemove); + + // Create and add new fields directly to the existing collection + for(CustomType type : newTypes) { + CustomField newField = new CustomField(); + newField.setType(type); + newField.setValue(type.getDefaultValue()); + em.persist(newField); + fields.add(newField); + } + + // Persist the assessment - the collection is already modified in-place + em.persist(assessment); + HibHelper.getInstance().commit(); + + System.out.println("CustomFields Updated to " + fields.size() + " fields"); + } + +} diff --git a/test/com/fuse/unittests/CryptoTests.java b/test/com/fuse/unittests/CryptoTests.java new file mode 100644 index 00000000..d1c3c94d --- /dev/null +++ b/test/com/fuse/unittests/CryptoTests.java @@ -0,0 +1,105 @@ +package com.fuse.unittests; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.spec.KeySpec; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.codec.binary.Base64; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.fuse.utils.FSUtils; + +/** + * Covers the at-rest encryption helpers in FSUtils: the AES-GCM format written + * today and the read-only fallback for values written by earlier releases in + * AES/ECB with a constant salt. + */ +public class CryptoTests { + + private static final String SECRET = "unit-test-secret-key"; + + @BeforeClass + public static void setSecret() { + if (System.getenv("FACTION_SECRET_KEY") == null) { + System.setProperty("FACTION_SECRET_KEY", SECRET); + } + } + + private static String secret() { + String env = System.getenv("FACTION_SECRET_KEY"); + return env == null ? SECRET : env; + } + + @Test + public void passwordRoundTrip() { + String encrypted = FSUtils.encryptPassword("p@ssw0rd with spaces and ünïcode"); + assertTrue("new values must use the GCM format", encrypted.startsWith("$AESGCM$")); + assertEquals("p@ssw0rd with spaces and ünïcode", FSUtils.decryptPassword(encrypted)); + } + + @Test + public void bytesRoundTrip() { + byte[] data = new byte[1024]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) i; + } + String encrypted = FSUtils.encryptBytes(data); + assertTrue(encrypted.startsWith("$AESGCM$")); + assertArrayEquals(data, FSUtils.decryptBytes(encrypted)); + } + + @Test + public void sameInputProducesDifferentCiphertext() { + String first = FSUtils.encryptPassword("same"); + String second = FSUtils.encryptPassword("same"); + assertNotEquals("random salt/IV must make ciphertexts unique", first, second); + assertEquals("same", FSUtils.decryptPassword(first)); + assertEquals("same", FSUtils.decryptPassword(second)); + } + + @Test + public void tamperedCiphertextIsRejected() { + String encrypted = FSUtils.encryptPassword("integrity"); + byte[] raw = Base64.decodeBase64(encrypted.substring("$AESGCM$".length())); + raw[raw.length - 1] ^= 0x01; // flip a bit in the GCM tag + String tampered = "$AESGCM$" + Base64.encodeBase64String(raw); + assertEquals("", FSUtils.decryptPassword(tampered)); + } + + @Test + public void legacyEcbValuesStillDecrypt() throws Exception { + // Reproduce exactly what encryptPassword() wrote before the GCM format existed. + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(secret().getBytes()); + char[] b64hash = Base64.encodeBase64String(hash).toCharArray(); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(b64hash, "f04ce910-bedb-4d8f-a023-4d2441dc0fba".getBytes(), 65536, 256); + SecretKey tmp = factory.generateSecret(spec); + SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); + Cipher legacy = Cipher.getInstance("AES/ECB/PKCS5Padding"); + legacy.init(Cipher.ENCRYPT_MODE, key); + String legacyValue = Base64.encodeBase64String(legacy.doFinal("old-smtp-password".getBytes(StandardCharsets.UTF_8))); + + assertEquals("old-smtp-password", FSUtils.decryptPassword(legacyValue)); + } + + @Test + public void garbageInputIsHandled() { + assertEquals("", FSUtils.decryptPassword(null)); + assertEquals("", FSUtils.decryptPassword("")); + assertEquals("", FSUtils.decryptPassword("$AESGCM$AAAA")); + assertEquals("", FSUtils.decryptPassword("not-base64-!!")); + } +} diff --git a/test/com/fuse/unittests/NotesXssSanitizationTest.java b/test/com/fuse/unittests/NotesXssSanitizationTest.java new file mode 100644 index 00000000..244cf5a6 --- /dev/null +++ b/test/com/fuse/unittests/NotesXssSanitizationTest.java @@ -0,0 +1,67 @@ +package com.fuse.unittests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.fuse.dao.Comment; +import com.fuse.dao.Vulnerability; + +/** + * Peer review "notes" fields are rendered into live suneditor rich-text editors + * on the TrackChanges page (peerreviewedit.js). Output-encoding the textarea does + * not help there, because the editor reads textarea.value (already entity-decoded) + * and injects it as innerHTML. The only defense is sanitizing the stored value, so + * these setters must strip active content the way the description/recommendation/ + * details setters already do. Comment.exportAssessment() reconstructs a vuln through + * these same setters on every display, so this also neutralizes notes that were + * stored before the fix. + */ +public class NotesXssSanitizationTest { + + private static final String IMG = ""; + private static final String SCRIPT = ""; + private static final String TRACKED = "ok added"; + + private static void assertStripped(String value) { + assertFalse("onerror handler must be removed: " + value, value.toLowerCase().contains("onerror")); + assertFalse("script tag must be removed: " + value, value.toLowerCase().contains("

"); + assertEquals("

", v.getDesc_notes()); + } +} diff --git a/test/com/fuse/unittests/UploadedFileGuardTest.java b/test/com/fuse/unittests/UploadedFileGuardTest.java new file mode 100644 index 00000000..302ef2b4 --- /dev/null +++ b/test/com/fuse/unittests/UploadedFileGuardTest.java @@ -0,0 +1,43 @@ +package com.fuse.unittests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.Test; + +import com.fuse.utils.FSUtils; + +/** FSUtils.checkUploadedFile must accept Struts multipart temp files and reject anything else. */ +public class UploadedFileGuardTest { + + @Test + public void acceptsFileInsideTempDir() throws Exception { + File f = Files.createTempFile("faction-upload", ".bin").toFile(); + try { + File ok = FSUtils.checkUploadedFile(f); + assertEquals(f.getCanonicalPath(), ok.getCanonicalPath()); + } finally { + f.delete(); + } + } + + @Test + public void rejectsFileOutsideTempDir() throws Exception { + File outside = new File("pom.xml").getAbsoluteFile(); // exists, but not an upload + try { + FSUtils.checkUploadedFile(outside); + fail("file outside the upload directory must be rejected"); + } catch (IOException expected) { + } + } + + @Test + public void rejectsMissingOrNull() throws Exception { + try { FSUtils.checkUploadedFile(null); fail("null must be rejected"); } catch (IOException expected) {} + try { FSUtils.checkUploadedFile(new File(System.getProperty("java.io.tmpdir"), "does-not-exist-" + System.nanoTime())); fail("missing file must be rejected"); } catch (IOException expected) {} + } +}