diff --git a/.dockerignore b/.dockerignore
index 70611bb5..92acb2bd 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,4 +2,5 @@
**/__pycache__
/tests
/docs
-/.github
\ No newline at end of file
+/.github
+/.git
diff --git a/.github/workflows/build_windows_exe.yml b/.github/workflows/build_windows_exe.yml
index 2343d81a..fcd078bf 100644
--- a/.github/workflows/build_windows_exe.yml
+++ b/.github/workflows/build_windows_exe.yml
@@ -30,6 +30,6 @@ jobs:
- name: Build Windows executable
run: python setup.py build_exe
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v4
with:
path: ./build/*
diff --git a/.github/workflows/release_to_docker.yml b/.github/workflows/release_to_docker.yml
index 10551e23..8a772e87 100644
--- a/.github/workflows/release_to_docker.yml
+++ b/.github/workflows/release_to_docker.yml
@@ -39,7 +39,7 @@ jobs:
with:
context: ./
file: ./Dockerfile
- platforms: linux/amd64
+ platforms: linux/amd64, linux/arm64
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
\ No newline at end of file
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 06a5054a..1afc47b1 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
+ python-version: [3.9, "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
@@ -34,7 +34,7 @@ jobs:
- name: Run linters
# Only do linting once
- if: matrix.python-version == 3.8
+ if: matrix.python-version == 3.9
run: python -m invoke lint
- name: Run tests
diff --git a/.github/workflows/run_tests_with_lowest_pydantic_version.yml b/.github/workflows/run_tests_with_lowest_pydantic_version.yml
index f4509748..fc95ccc1 100644
--- a/.github/workflows/run_tests_with_lowest_pydantic_version.yml
+++ b/.github/workflows/run_tests_with_lowest_pydantic_version.yml
@@ -15,15 +15,15 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: 3.8
+ python-version: 3.9
- name: Install sslyze dependencies
run: |
python -m pip install --upgrade pip setuptools
python -m pip install -e .
- - name: Install pydantic 2.2
- run: python -m pip install "pydantic==2.2"
+ - name: Install pydantic 2.3
+ run: python -m pip install "pydantic==2.3"
- name: Install dev dependencies
run: python -m pip install -r requirements-dev.txt
diff --git a/.github/workflows/scan_apache2_server.yml b/.github/workflows/scan_apache2_server.yml
index db1c6d86..7415abf6 100644
--- a/.github/workflows/scan_apache2_server.yml
+++ b/.github/workflows/scan_apache2_server.yml
@@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: 3.8
+ python-version: 3.9
- name: Install Apache2
run: |
diff --git a/.github/workflows/scan_iis_server.yml b/.github/workflows/scan_iis_server.yml
index 6e9fab9e..ab52d1b2 100644
--- a/.github/workflows/scan_iis_server.yml
+++ b/.github/workflows/scan_iis_server.yml
@@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: 3.8
+ python-version: 3.9
- name: Install IIS
run: |
diff --git a/.github/workflows/scan_nginx_server.yml b/.github/workflows/scan_nginx_server.yml
index 485fcf2b..ac0b14a9 100644
--- a/.github/workflows/scan_nginx_server.yml
+++ b/.github/workflows/scan_nginx_server.yml
@@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: 3.8
+ python-version: 3.9
- name: Install Nginx
run: |
diff --git a/.github/workflows/test_module_setup.yml b/.github/workflows/test_module_setup.yml
index 789dc3d3..e727cd9a 100644
--- a/.github/workflows/test_module_setup.yml
+++ b/.github/workflows/test_module_setup.yml
@@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: 3.8
+ python-version: 3.9
- name: Install pip
run: |
diff --git a/Dockerfile b/Dockerfile
index 82e387a4..2930b050 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,23 @@
-FROM python:3.9-slim
+# set python version
+ARG PYTHON_VERSION="3.12"
+
+FROM docker.io/python:${PYTHON_VERSION}-slim AS build
COPY . /sslyze/
-# install latest updates as root
-RUN apt-get update \
- && apt-get install -y sudo
+WORKDIR /sslyze
+# use a venv
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
# install sslyze based on sourcecode
-RUN python -m pip install --upgrade pip setuptools wheel
-RUN python /sslyze/setup.py install
+RUN pip install --upgrade pip setuptools wheel
+RUN pip install .
+
+FROM docker.io/python:${PYTHON_VERSION}-slim AS run
# set user to a non-root user sslyze
RUN adduser --no-create-home --disabled-password --gecos "" --uid 1001 sslyze
USER sslyze
-# restrict execution to sslyze
WORKDIR /sslyze
-ENTRYPOINT ["python", "-m", "sslyze"]
-CMD ["-h"]
\ No newline at end of file
+# copy sslyze from build stage
+COPY --from=build /opt/venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+ENTRYPOINT ["sslyze"]
+CMD ["-h"]
diff --git a/README.md b/README.md
index e19044c1..7bfa66cb 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ $ python -m sslyze www.yahoo.com www.google.com "[2607:f8b0:400a:807::2004]:443"
It can also be used via Docker:
```
-$ docker run --rm -it nablac0d3/sslyze:6.0.0 www.google.com
+$ docker run --rm -it nablac0d3/sslyze:6.1.0 www.google.com
```
Lastly, a pre-compiled Windows executable can be downloaded from [the Releases
@@ -104,7 +104,7 @@ $ invoke test
License
-------
-Copyright (c) 2024 Alban Diquet
+Copyright (c) 2025 Alban Diquet
SSLyze is made available under the terms of the GNU Affero General Public License (AGPL). See LICENSE.txt for details and exceptions.
diff --git a/api_sample.py b/api_sample.py
index 31bf2fc6..fe4a5af4 100755
--- a/api_sample.py
+++ b/api_sample.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timezone
from pathlib import Path
from typing import List
@@ -25,7 +25,7 @@ def _print_failed_scan_command_attempt(scan_command_attempt: ScanCommandAttempt)
def main() -> None:
print("=> Starting the scans")
- date_scans_started = datetime.utcnow()
+ date_scans_started = datetime.now(timezone.utc)
# First create the scan requests for each server that we want to scan
try:
@@ -104,7 +104,7 @@ def main() -> None:
# Lastly, save the all the results to a JSON file
json_file_out = Path("api_sample_results.json")
print(f"\n\n=> Saving scan results to {json_file_out}")
- example_json_result_output(json_file_out, all_server_scan_results, date_scans_started, datetime.utcnow())
+ example_json_result_output(json_file_out, all_server_scan_results, date_scans_started, datetime.now(timezone.utc))
# And ensure we are able to parse them
print(f"\n\n=> Parsing scan results from {json_file_out}")
diff --git a/docs/available-scan-commands.rst b/docs/available-scan-commands.rst
index 37c30da8..6a080266 100644
--- a/docs/available-scan-commands.rst
+++ b/docs/available-scan-commands.rst
@@ -149,7 +149,6 @@ Result class
.. autoclass:: HttpHeadersScanResult
.. autoclass:: StrictTransportSecurityHeader
-.. autoclass:: ExpectCtHeader
OpenSSL CCS Injection
*********************
@@ -170,3 +169,14 @@ Result class
============
.. autoclass:: SessionRenegotiationScanResult
+
+
+Extended Master Secret
+**********************
+
+**ScanCommand.TLS_EXTENDED_MASTER_SECRET**: Test a server for TLS Extended Master Secret extension support.
+
+Result class
+============
+
+.. autoclass:: EmsExtensionScanResult
diff --git a/docs/conf.py b/docs/conf.py
index 068095f6..b3571596 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -70,7 +70,7 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
diff --git a/docs/documentation/.buildinfo b/docs/documentation/.buildinfo
index 50304dbd..66eec8a8 100644
--- a/docs/documentation/.buildinfo
+++ b/docs/documentation/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
-# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: f71b8d947628f4c16f8ec17ec85934ab
+# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: 094768c3cd394a7960733fba4a2e0033
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/documentation/_static/alabaster.css b/docs/documentation/_static/alabaster.css
index 517d0b29..7e75bf8f 100644
--- a/docs/documentation/_static/alabaster.css
+++ b/docs/documentation/_static/alabaster.css
@@ -1,5 +1,3 @@
-@import url("basic.css");
-
/* -- page layout ----------------------------------------------------------- */
body {
@@ -69,6 +67,11 @@ div.relations {
}
+div.sphinxsidebar {
+ max-height: 100%;
+ overflow-y: auto;
+}
+
div.sphinxsidebar a {
color: #444;
text-decoration: none;
@@ -155,6 +158,14 @@ div.sphinxsidebar input {
font-size: 1em;
}
+div.sphinxsidebar #searchbox {
+ margin: 1em 0;
+}
+
+div.sphinxsidebar .search > div {
+ display: table-cell;
+}
+
div.sphinxsidebar hr {
border: none;
height: 1px;
@@ -250,10 +261,6 @@ div.admonition p.last {
margin-bottom: 0;
}
-div.highlight {
- background-color: #fff;
-}
-
dt:target, .highlight {
background: #FAF3E8;
}
@@ -441,7 +448,7 @@ ul, ol {
}
pre {
- background: #EEE;
+ background: unset;
padding: 7px 30px;
margin: 15px 0px;
line-height: 1.3em;
@@ -472,15 +479,15 @@ a.reference {
border-bottom: 1px dotted #004B6B;
}
+a.reference:hover {
+ border-bottom: 1px solid #6D4100;
+}
+
/* Don't put an underline on images */
a.image-reference, a.image-reference:hover {
border-bottom: none;
}
-a.reference:hover {
- border-bottom: 1px solid #6D4100;
-}
-
a.footnote-reference {
text-decoration: none;
font-size: 0.7em;
@@ -496,68 +503,7 @@ a:hover tt, a:hover code {
background: #EEE;
}
-
-@media screen and (max-width: 870px) {
-
- div.sphinxsidebar {
- display: none;
- }
-
- div.document {
- width: 100%;
-
- }
-
- div.documentwrapper {
- margin-left: 0;
- margin-top: 0;
- margin-right: 0;
- margin-bottom: 0;
- }
-
- div.bodywrapper {
- margin-top: 0;
- margin-right: 0;
- margin-bottom: 0;
- margin-left: 0;
- }
-
- ul {
- margin-left: 0;
- }
-
- li > ul {
- /* Matches the 30px from the "ul, ol" selector above */
- margin-left: 30px;
- }
-
- .document {
- width: auto;
- }
-
- .footer {
- width: auto;
- }
-
- .bodywrapper {
- margin: 0;
- }
-
- .footer {
- width: auto;
- }
-
- .github {
- display: none;
- }
-
-
-
-}
-
-
-
-@media screen and (max-width: 875px) {
+@media screen and (max-width: 940px) {
body {
margin: 0;
@@ -567,12 +513,16 @@ a:hover tt, a:hover code {
div.documentwrapper {
float: none;
background: #fff;
+ margin-left: 0;
+ margin-top: 0;
+ margin-right: 0;
+ margin-bottom: 0;
}
div.sphinxsidebar {
display: block;
float: none;
- width: 102.5%;
+ width: unset;
margin: 50px -30px -20px -30px;
padding: 10px 20px;
background: #333;
@@ -607,8 +557,14 @@ a:hover tt, a:hover code {
div.body {
min-height: 0;
+ min-width: auto; /* fixes width on small screens, breaks .hll */
padding: 0;
}
+
+ .hll {
+ /* "fixes" the breakage */
+ width: max-content;
+ }
.rtd_doc_footer {
display: none;
@@ -622,13 +578,18 @@ a:hover tt, a:hover code {
width: auto;
}
- .footer {
- width: auto;
- }
-
.github {
display: none;
}
+
+ ul {
+ margin-left: 0;
+ }
+
+ li > ul {
+ /* Matches the 30px from the "ul, ol" selector above */
+ margin-left: 30px;
+ }
}
@@ -638,15 +599,7 @@ a:hover tt, a:hover code {
display: none!important;
}
-/* Make nested-list/multi-paragraph items look better in Releases changelog
- * pages. Without this, docutils' magical list fuckery causes inconsistent
- * formatting between different release sub-lists.
- */
-div#changelog > div.section > ul > li > p:only-child {
- margin-bottom: 0;
-}
-
-/* Hide fugly table cell borders in ..bibliography:: directive output */
+/* Hide ugly table cell borders in ..bibliography:: directive output */
table.docutils.citation, table.docutils.citation td, table.docutils.citation th {
border: none;
/* Below needed in some edge cases; if not applied, bottom shadows appear */
@@ -700,4 +653,11 @@ nav#breadcrumbs li+li:before {
div.related {
display: none;
}
+}
+
+img.github {
+ position: absolute;
+ top: 0;
+ border: 0;
+ right: 0;
}
\ No newline at end of file
diff --git a/docs/documentation/_static/basic.css b/docs/documentation/_static/basic.css
index 30fee9d0..d9846dac 100644
--- a/docs/documentation/_static/basic.css
+++ b/docs/documentation/_static/basic.css
@@ -1,12 +1,5 @@
/*
- * basic.css
- * ~~~~~~~~~
- *
* Sphinx stylesheet -- basic theme.
- *
- * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
*/
/* -- main layout ----------------------------------------------------------- */
@@ -115,15 +108,11 @@ img {
/* -- search page ----------------------------------------------------------- */
ul.search {
- margin: 10px 0 0 20px;
- padding: 0;
+ margin-top: 10px;
}
ul.search li {
- padding: 5px 0 5px 20px;
- background-image: url(file.png);
- background-repeat: no-repeat;
- background-position: 0 7px;
+ padding: 5px 0;
}
ul.search li a {
@@ -222,7 +211,7 @@ table.modindextable td {
/* -- general body styles --------------------------------------------------- */
div.body {
- min-width: 360px;
+ min-width: inherit;
max-width: 800px;
}
diff --git a/docs/documentation/_static/doctools.js b/docs/documentation/_static/doctools.js
index d06a71d7..0398ebb9 100644
--- a/docs/documentation/_static/doctools.js
+++ b/docs/documentation/_static/doctools.js
@@ -1,12 +1,5 @@
/*
- * doctools.js
- * ~~~~~~~~~~~
- *
* Base JavaScript utilities for all Sphinx HTML documentation.
- *
- * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
*/
"use strict";
diff --git a/docs/documentation/_static/documentation_options.js b/docs/documentation/_static/documentation_options.js
index 7cebf3b0..668036d2 100644
--- a/docs/documentation/_static/documentation_options.js
+++ b/docs/documentation/_static/documentation_options.js
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
- VERSION: '6.0.0',
+ VERSION: '6.1.0',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
diff --git a/docs/documentation/_static/github-banner.svg b/docs/documentation/_static/github-banner.svg
new file mode 100644
index 00000000..c47d9dc0
--- /dev/null
+++ b/docs/documentation/_static/github-banner.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/docs/documentation/_static/language_data.js b/docs/documentation/_static/language_data.js
index 250f5665..c7fe6c6f 100644
--- a/docs/documentation/_static/language_data.js
+++ b/docs/documentation/_static/language_data.js
@@ -1,19 +1,12 @@
/*
- * language_data.js
- * ~~~~~~~~~~~~~~~~
- *
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
- *
- * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
*/
var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];
-/* Non-minified version is copied as a separate JS file, is available */
+/* Non-minified version is copied as a separate JS file, if available */
/**
* Porter Stemmer
diff --git a/docs/documentation/_static/searchtools.js b/docs/documentation/_static/searchtools.js
index 7918c3fa..2c774d17 100644
--- a/docs/documentation/_static/searchtools.js
+++ b/docs/documentation/_static/searchtools.js
@@ -1,12 +1,5 @@
/*
- * searchtools.js
- * ~~~~~~~~~~~~~~~~
- *
* Sphinx JavaScript utilities for the full-text search.
- *
- * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
*/
"use strict";
@@ -20,7 +13,7 @@ if (typeof Scorer === "undefined") {
// and returns the new score.
/*
score: result => {
- const [docname, title, anchor, descr, score, filename] = result
+ const [docname, title, anchor, descr, score, filename, kind] = result
return score
},
*/
@@ -47,6 +40,14 @@ if (typeof Scorer === "undefined") {
};
}
+// Global search result kind enum, used by themes to style search results.
+class SearchResultKind {
+ static get index() { return "index"; }
+ static get object() { return "object"; }
+ static get text() { return "text"; }
+ static get title() { return "title"; }
+}
+
const _removeChildren = (element) => {
while (element && element.lastChild) element.removeChild(element.lastChild);
};
@@ -64,9 +65,13 @@ const _displayItem = (item, searchTerms, highlightTerms) => {
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
const contentRoot = document.documentElement.dataset.content_root;
- const [docName, title, anchor, descr, score, _filename] = item;
+ const [docName, title, anchor, descr, score, _filename, kind] = item;
let listItem = document.createElement("li");
+ // Add a class representing the item's type:
+ // can be used by a theme's CSS selector for styling
+ // See SearchResultKind for the class names.
+ listItem.classList.add(`kind-${kind}`);
let requestUrl;
let linkUrl;
if (docBuilder === "dirhtml") {
@@ -99,7 +104,7 @@ const _displayItem = (item, searchTerms, highlightTerms) => {
.then((data) => {
if (data)
listItem.appendChild(
- Search.makeSearchSummary(data, searchTerms)
+ Search.makeSearchSummary(data, searchTerms, anchor)
);
// highlight search terms in the summary
if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
@@ -115,9 +120,11 @@ const _finishSearch = (resultCount) => {
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
);
else
- Search.status.innerText = _(
- `Search finished, found ${resultCount} page(s) matching the search query.`
- );
+ Search.status.innerText = Documentation.ngettext(
+ "Search finished, found one page matching the search query.",
+ "Search finished, found ${resultCount} pages matching the search query.",
+ resultCount,
+ ).replace('${resultCount}', resultCount);
};
const _displayNextItem = (
results,
@@ -137,6 +144,22 @@ const _displayNextItem = (
// search finished, update title and status message
else _finishSearch(resultCount);
};
+// Helper function used by query() to order search results.
+// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
+// Order the results by score (in opposite order of appearance, since the
+// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
+const _orderResultsByScoreThenName = (a, b) => {
+ const leftScore = a[4];
+ const rightScore = b[4];
+ if (leftScore === rightScore) {
+ // same score: sort alphabetically
+ const leftTitle = a[1].toLowerCase();
+ const rightTitle = b[1].toLowerCase();
+ if (leftTitle === rightTitle) return 0;
+ return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
+ }
+ return leftScore > rightScore ? 1 : -1;
+};
/**
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
@@ -160,13 +183,26 @@ const Search = {
_queued_query: null,
_pulse_status: -1,
- htmlToText: (htmlString) => {
+ htmlToText: (htmlString, anchor) => {
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
- htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() });
+ for (const removalQuery of [".headerlink", "script", "style"]) {
+ htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });
+ }
+ if (anchor) {
+ const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`);
+ if (anchorContent) return anchorContent.textContent;
+
+ console.warn(
+ `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`
+ );
+ }
+
+ // if anchor not specified or not found, fall back to main content
const docContent = htmlElement.querySelector('[role="main"]');
- if (docContent !== undefined) return docContent.textContent;
+ if (docContent) return docContent.textContent;
+
console.warn(
- "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template."
+ "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template."
);
return "";
},
@@ -219,6 +255,7 @@ const Search = {
searchSummary.classList.add("search-summary");
searchSummary.innerText = "";
const searchList = document.createElement("ul");
+ searchList.setAttribute("role", "list");
searchList.classList.add("search");
const out = document.getElementById("search-results");
@@ -239,16 +276,7 @@ const Search = {
else Search.deferQuery(query);
},
- /**
- * execute search (requires search index to be loaded)
- */
- query: (query) => {
- const filenames = Search._index.filenames;
- const docNames = Search._index.docnames;
- const titles = Search._index.titles;
- const allTitles = Search._index.alltitles;
- const indexEntries = Search._index.indexentries;
-
+ _parseQuery: (query) => {
// stem the search terms and add them to the correct list
const stemmer = new Stemmer();
const searchTerms = new Set();
@@ -284,22 +312,40 @@ const Search = {
// console.info("required: ", [...searchTerms]);
// console.info("excluded: ", [...excludedTerms]);
- // array of [docname, title, anchor, descr, score, filename]
- let results = [];
+ return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];
+ },
+
+ /**
+ * execute search (requires search index to be loaded)
+ */
+ _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => {
+ const filenames = Search._index.filenames;
+ const docNames = Search._index.docnames;
+ const titles = Search._index.titles;
+ const allTitles = Search._index.alltitles;
+ const indexEntries = Search._index.indexentries;
+
+ // Collect multiple result groups to be sorted separately and then ordered.
+ // Each is an array of [docname, title, anchor, descr, score, filename, kind].
+ const normalResults = [];
+ const nonMainIndexResults = [];
+
_removeChildren(document.getElementById("search-progress"));
- const queryLower = query.toLowerCase();
+ const queryLower = query.toLowerCase().trim();
for (const [title, foundTitles] of Object.entries(allTitles)) {
- if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) {
+ if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
for (const [file, id] of foundTitles) {
- let score = Math.round(100 * queryLower.length / title.length)
- results.push([
+ const score = Math.round(Scorer.title * queryLower.length / title.length);
+ const boost = titles[file] === title ? 1 : 0; // add a boost for document titles
+ normalResults.push([
docNames[file],
titles[file] !== title ? `${titles[file]} > ${title}` : title,
id !== null ? "#" + id : "",
null,
- score,
+ score + boost,
filenames[file],
+ SearchResultKind.title,
]);
}
}
@@ -308,46 +354,48 @@ const Search = {
// search for explicit entries in index directives
for (const [entry, foundEntries] of Object.entries(indexEntries)) {
if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
- for (const [file, id] of foundEntries) {
- let score = Math.round(100 * queryLower.length / entry.length)
- results.push([
+ for (const [file, id, isMain] of foundEntries) {
+ const score = Math.round(100 * queryLower.length / entry.length);
+ const result = [
docNames[file],
titles[file],
id ? "#" + id : "",
null,
score,
filenames[file],
- ]);
+ SearchResultKind.index,
+ ];
+ if (isMain) {
+ normalResults.push(result);
+ } else {
+ nonMainIndexResults.push(result);
+ }
}
}
}
// lookup as object
objectTerms.forEach((term) =>
- results.push(...Search.performObjectSearch(term, objectTerms))
+ normalResults.push(...Search.performObjectSearch(term, objectTerms))
);
// lookup as search terms in fulltext
- results.push(...Search.performTermsSearch(searchTerms, excludedTerms));
+ normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms));
// let the scorer override scores with a custom scoring function
- if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item)));
-
- // now sort the results by score (in opposite order of appearance, since the
- // display function below uses pop() to retrieve items) and then
- // alphabetically
- results.sort((a, b) => {
- const leftScore = a[4];
- const rightScore = b[4];
- if (leftScore === rightScore) {
- // same score: sort alphabetically
- const leftTitle = a[1].toLowerCase();
- const rightTitle = b[1].toLowerCase();
- if (leftTitle === rightTitle) return 0;
- return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
- }
- return leftScore > rightScore ? 1 : -1;
- });
+ if (Scorer.score) {
+ normalResults.forEach((item) => (item[4] = Scorer.score(item)));
+ nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
+ }
+
+ // Sort each group of results by score and then alphabetically by name.
+ normalResults.sort(_orderResultsByScoreThenName);
+ nonMainIndexResults.sort(_orderResultsByScoreThenName);
+
+ // Combine the result groups in (reverse) order.
+ // Non-main index entries are typically arbitrary cross-references,
+ // so display them after other results.
+ let results = [...nonMainIndexResults, ...normalResults];
// remove duplicate search results
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
@@ -361,7 +409,12 @@ const Search = {
return acc;
}, []);
- results = results.reverse();
+ return results.reverse();
+ },
+
+ query: (query) => {
+ const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query);
+ const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms);
// for debugging
//Search.lastresults = results.slice(); // a copy
@@ -432,6 +485,7 @@ const Search = {
descr,
score,
filenames[match[0]],
+ SearchResultKind.object,
]);
};
Object.keys(objects).forEach((prefix) =>
@@ -466,14 +520,18 @@ const Search = {
// add support for partial matches
if (word.length > 2) {
const escapedWord = _escapeRegExp(word);
- Object.keys(terms).forEach((term) => {
- if (term.match(escapedWord) && !terms[word])
- arr.push({ files: terms[term], score: Scorer.partialTerm });
- });
- Object.keys(titleTerms).forEach((term) => {
- if (term.match(escapedWord) && !titleTerms[word])
- arr.push({ files: titleTerms[word], score: Scorer.partialTitle });
- });
+ if (!terms.hasOwnProperty(word)) {
+ Object.keys(terms).forEach((term) => {
+ if (term.match(escapedWord))
+ arr.push({ files: terms[term], score: Scorer.partialTerm });
+ });
+ }
+ if (!titleTerms.hasOwnProperty(word)) {
+ Object.keys(titleTerms).forEach((term) => {
+ if (term.match(escapedWord))
+ arr.push({ files: titleTerms[term], score: Scorer.partialTitle });
+ });
+ }
}
// no match but word was a required one
@@ -496,9 +554,8 @@ const Search = {
// create the mapping
files.forEach((file) => {
- if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1)
- fileMap.get(file).push(word);
- else fileMap.set(file, [word]);
+ if (!fileMap.has(file)) fileMap.set(file, [word]);
+ else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word);
});
});
@@ -539,6 +596,7 @@ const Search = {
null,
score,
filenames[file],
+ SearchResultKind.text,
]);
}
return results;
@@ -549,8 +607,8 @@ const Search = {
* search summary for a given text. keywords is a list
* of stemmed words.
*/
- makeSearchSummary: (htmlText, keywords) => {
- const text = Search.htmlToText(htmlText);
+ makeSearchSummary: (htmlText, keywords, anchor) => {
+ const text = Search.htmlToText(htmlText, anchor);
if (text === "") return null;
const textLower = text.toLowerCase();
diff --git a/docs/documentation/available-scan-commands.html b/docs/documentation/available-scan-commands.html
index 14baa54b..7faa3d54 100644
--- a/docs/documentation/available-scan-commands.html
+++ b/docs/documentation/available-scan-commands.html
@@ -3,13 +3,14 @@
-
+
- Appendix: Scan Commands — SSLyze 6.0.0 documentation
+ Appendix: Scan Commands — SSLyze 6.1.0 documentation
-
-
-
+
+
+
+
@@ -17,8 +18,9 @@
+
+
-
@@ -31,7 +33,7 @@
-