susbot 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. susbot-2.0.1/LICENSE.md +75 -0
  2. susbot-2.0.1/PKG-INFO +74 -0
  3. susbot-2.0.1/README.md +50 -0
  4. susbot-2.0.1/cli/Cargo.toml +30 -0
  5. susbot-2.0.1/cli/LICENSE.md +75 -0
  6. susbot-2.0.1/cli/README.md +46 -0
  7. susbot-2.0.1/cli/src/audit.rs +234 -0
  8. susbot-2.0.1/cli/src/crawl.rs +201 -0
  9. susbot-2.0.1/cli/src/diff_cmd.rs +58 -0
  10. susbot-2.0.1/cli/src/lib.rs +92 -0
  11. susbot-2.0.1/cli/src/main.rs +5 -0
  12. susbot-2.0.1/cli/src/net.rs +103 -0
  13. susbot-2.0.1/cli/src/track.rs +364 -0
  14. susbot-2.0.1/core/Cargo.toml +26 -0
  15. susbot-2.0.1/core/LICENSE.md +75 -0
  16. susbot-2.0.1/core/README.md +25 -0
  17. susbot-2.0.1/core/data/default.toml +1175 -0
  18. susbot-2.0.1/core/data/en.json +822 -0
  19. susbot-2.0.1/core/src/agents.rs +23 -0
  20. susbot-2.0.1/core/src/ai_status.rs +114 -0
  21. susbot-2.0.1/core/src/analyser.rs +269 -0
  22. susbot-2.0.1/core/src/analysis.rs +148 -0
  23. susbot-2.0.1/core/src/checks.rs +227 -0
  24. susbot-2.0.1/core/src/config.rs +539 -0
  25. susbot-2.0.1/core/src/diff.rs +283 -0
  26. susbot-2.0.1/core/src/export/csv.rs +201 -0
  27. susbot-2.0.1/core/src/export/markdown.rs +509 -0
  28. susbot-2.0.1/core/src/export/mod.rs +5 -0
  29. susbot-2.0.1/core/src/fetch.rs +77 -0
  30. susbot-2.0.1/core/src/i18n.rs +173 -0
  31. susbot-2.0.1/core/src/lib.rs +37 -0
  32. susbot-2.0.1/core/src/model.rs +259 -0
  33. susbot-2.0.1/core/src/parser.rs +298 -0
  34. susbot-2.0.1/core/src/plural.rs +139 -0
  35. susbot-2.0.1/core/src/recon/api.rs +46 -0
  36. susbot-2.0.1/core/src/recon/cloud.rs +115 -0
  37. susbot-2.0.1/core/src/recon/cms.rs +93 -0
  38. susbot-2.0.1/core/src/recon/comments.rs +127 -0
  39. susbot-2.0.1/core/src/recon/data.rs +118 -0
  40. susbot-2.0.1/core/src/recon/extensions.rs +112 -0
  41. susbot-2.0.1/core/src/recon/hosts.rs +145 -0
  42. susbot-2.0.1/core/src/recon/mod.rs +78 -0
  43. susbot-2.0.1/core/src/report.rs +324 -0
  44. susbot-2.0.1/core/src/security.rs +74 -0
  45. susbot-2.0.1/core/src/url_util.rs +123 -0
  46. susbot-2.0.1/pyproject.toml +38 -0
  47. susbot-2.0.1/python/Cargo.lock +816 -0
  48. susbot-2.0.1/python/Cargo.toml +37 -0
  49. susbot-2.0.1/python/LICENSE.md +75 -0
  50. susbot-2.0.1/python/README.md +50 -0
  51. susbot-2.0.1/python/src/lib.rs +123 -0
  52. susbot-2.0.1/python/susbot/__init__.py +193 -0
  53. susbot-2.0.1/python/susbot/__main__.py +30 -0
  54. susbot-2.0.1/python/susbot/_susbot.pyi +25 -0
  55. susbot-2.0.1/python/susbot/py.typed +0 -0
  56. susbot-2.0.1/python/tests/test_susbot.py +99 -0
@@ -0,0 +1,75 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
12
+
13
+ ## Distribution License
14
+
15
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
16
+
17
+ ## Notices
18
+
19
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
20
+
21
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
22
+
23
+ ## Changes and New Works License
24
+
25
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
26
+
27
+ ## Patent License
28
+
29
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
30
+
31
+ ## Noncommercial Purposes
32
+
33
+ Any noncommercial purpose is a permitted purpose.
34
+
35
+ ## Personal Uses
36
+
37
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
38
+
39
+ ## Noncommercial Organizations
40
+
41
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
42
+
43
+ ## Fair Use
44
+
45
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
46
+
47
+ ## No Other Rights
48
+
49
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
50
+
51
+ ## Patent Defense
52
+
53
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
54
+
55
+ ## Violations
56
+
57
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
58
+
59
+ ## No Liability
60
+
61
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
62
+
63
+ ## Definitions
64
+
65
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
66
+
67
+ **You** refers to the individual or entity agreeing to these terms.
68
+
69
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
70
+
71
+ **Your licenses** are all the licenses granted to you for the software under these terms.
72
+
73
+ **Use** means anything you do with the software requiring one of your licenses.
74
+
75
+ Required Notice: Copyright 2026 Sitefig (https://sitefig.eu)
susbot-2.0.1/PKG-INFO ADDED
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: susbot
3
+ Version: 2.0.1
4
+ Classifier: Development Status :: 5 - Production/Stable
5
+ Classifier: Environment :: Console
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Rust
10
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
11
+ Classifier: Typing :: Typed
12
+ License-File: LICENSE.md
13
+ Summary: robots.txt checker: which search engines and AI crawlers a site lets in, lint findings, exposed paths and diffs (sus.bot). Python bindings and the susbot command.
14
+ Keywords: robots.txt,crawler,seo,ai,gptbot,rfc9309,audit
15
+ Home-Page: https://sus.bot/
16
+ Author: Sitefig
17
+ License-Expression: PolyForm-Noncommercial-1.0.0
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
20
+ Project-URL: Homepage, https://sus.bot/
21
+ Project-URL: Issues, https://github.com/sitefig/robots/issues
22
+ Project-URL: Source, https://github.com/sitefig/robots
23
+
24
+ # susbot
25
+
26
+ The Python package of [sus.bot](https://sus.bot/), a robots.txt checker. It tells you which search engines and AI crawlers a site lets in, what is wrong with the file, and which paths it gives away. It ships the Rust engine behind sus.bot as Python bindings and the `susbot` command.
27
+
28
+ ```
29
+ pip install susbot
30
+ ```
31
+
32
+ Wheels are built for Linux, macOS and Windows on CPython 3.10 and later.
33
+
34
+ ## Command line
35
+
36
+ ```
37
+ susbot https://example.com # audit a live site
38
+ susbot robots.txt --format markdown --out audit.md
39
+ susbot diff old.txt new.txt # what changed, crawler by crawler
40
+ python -m susbot --help
41
+ ```
42
+
43
+ It is the same program as `cargo install susbot`. Exit codes: 0 on success, 1 when a `--fail-on` or `--fail-on-security` threshold is met, 2 on errors.
44
+
45
+ ## Python
46
+
47
+ ```python
48
+ import susbot
49
+
50
+ text = open("robots.txt", encoding="utf-8").read()
51
+ a = susbot.Analysis(text, site_url="https://example.com/")
52
+
53
+ a.allowed("GPTBot", "/blog/post") # False
54
+ a.check_access("Googlebot", "/admin/") # the verdict and the rule that decided it
55
+ a.report["summary"] # counts, default policy, AI training, platform
56
+ a.issues # lint findings with stable ids
57
+ a.security # disallowed paths that look sensitive
58
+ a.markdown() # the client audit; also html(), csv_tabs(), tsv()
59
+
60
+ old = susbot.Analysis(old_text, site_url="https://example.com/")
61
+ susbot.diff(old, a) # crawler verdict flips, new sensitive paths, issues
62
+ ```
63
+
64
+ `Analysis` takes `config=` for a TOML file merged over `susbot.default_config()`, and `locale=` with `lang=` for a translated report. The report follows the [published JSON schema](https://sus.bot/schema/report.schema.json). The package never fetches anything itself; the `susbot` command does.
65
+
66
+ ## Links
67
+
68
+ - Website: [sus.bot](https://sus.bot/)
69
+ - Source, GitHub Action and issues: [github.com/sitefig/robots](https://github.com/sitefig/robots)
70
+
71
+ ## Licence
72
+
73
+ [PolyForm Noncommercial 1.0.0](LICENSE.md): free for noncommercial use. Commercial use needs a licence from [Sitefig](https://sitefig.eu/).
74
+
susbot-2.0.1/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # susbot
2
+
3
+ The Python package of [sus.bot](https://sus.bot/), a robots.txt checker. It tells you which search engines and AI crawlers a site lets in, what is wrong with the file, and which paths it gives away. It ships the Rust engine behind sus.bot as Python bindings and the `susbot` command.
4
+
5
+ ```
6
+ pip install susbot
7
+ ```
8
+
9
+ Wheels are built for Linux, macOS and Windows on CPython 3.10 and later.
10
+
11
+ ## Command line
12
+
13
+ ```
14
+ susbot https://example.com # audit a live site
15
+ susbot robots.txt --format markdown --out audit.md
16
+ susbot diff old.txt new.txt # what changed, crawler by crawler
17
+ python -m susbot --help
18
+ ```
19
+
20
+ It is the same program as `cargo install susbot`. Exit codes: 0 on success, 1 when a `--fail-on` or `--fail-on-security` threshold is met, 2 on errors.
21
+
22
+ ## Python
23
+
24
+ ```python
25
+ import susbot
26
+
27
+ text = open("robots.txt", encoding="utf-8").read()
28
+ a = susbot.Analysis(text, site_url="https://example.com/")
29
+
30
+ a.allowed("GPTBot", "/blog/post") # False
31
+ a.check_access("Googlebot", "/admin/") # the verdict and the rule that decided it
32
+ a.report["summary"] # counts, default policy, AI training, platform
33
+ a.issues # lint findings with stable ids
34
+ a.security # disallowed paths that look sensitive
35
+ a.markdown() # the client audit; also html(), csv_tabs(), tsv()
36
+
37
+ old = susbot.Analysis(old_text, site_url="https://example.com/")
38
+ susbot.diff(old, a) # crawler verdict flips, new sensitive paths, issues
39
+ ```
40
+
41
+ `Analysis` takes `config=` for a TOML file merged over `susbot.default_config()`, and `locale=` with `lang=` for a translated report. The report follows the [published JSON schema](https://sus.bot/schema/report.schema.json). The package never fetches anything itself; the `susbot` command does.
42
+
43
+ ## Links
44
+
45
+ - Website: [sus.bot](https://sus.bot/)
46
+ - Source, GitHub Action and issues: [github.com/sitefig/robots](https://github.com/sitefig/robots)
47
+
48
+ ## Licence
49
+
50
+ [PolyForm Noncommercial 1.0.0](LICENSE.md): free for noncommercial use. Commercial use needs a licence from [Sitefig](https://sitefig.eu/).
@@ -0,0 +1,30 @@
1
+ [package]
2
+ name = "susbot-cli"
3
+ description = "robots.txt checker for the command line and CI: which search engines and AI crawlers a site lets in, lint findings, exposed paths, diffs (sus.bot)"
4
+ version = "2.0.1"
5
+ edition = "2021"
6
+ license = "PolyForm-Noncommercial-1.0.0"
7
+ repository = "https://github.com/sitefig/robots"
8
+ homepage = "https://sus.bot/"
9
+ authors = ["Sitefig <https://sitefig.eu/>"]
10
+ readme = "README.md"
11
+ # Tests read files from the repository (examples, schema), so they stay out of the package.
12
+ exclude = ["tests/"]
13
+ keywords = ["robots-txt", "crawler", "seo", "ai", "audit"]
14
+ categories = ["command-line-utilities", "web-programming", "development-tools"]
15
+
16
+ [lib]
17
+ path = "src/lib.rs"
18
+
19
+ [[bin]]
20
+ name = "susbot"
21
+ path = "src/main.rs"
22
+
23
+ [dependencies]
24
+ susbot-core = { path = "../core", version = "2.0.1" }
25
+ serde = { version = "^1", features = ["derive"] }
26
+ serde_json = "^1"
27
+ clap = { version = "4", features = ["derive"] }
28
+ ureq = { version = "2", default-features = false, features = ["tls"] }
29
+ url = "^2"
30
+ flate2 = "1"
@@ -0,0 +1,75 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
12
+
13
+ ## Distribution License
14
+
15
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
16
+
17
+ ## Notices
18
+
19
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
20
+
21
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
22
+
23
+ ## Changes and New Works License
24
+
25
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
26
+
27
+ ## Patent License
28
+
29
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
30
+
31
+ ## Noncommercial Purposes
32
+
33
+ Any noncommercial purpose is a permitted purpose.
34
+
35
+ ## Personal Uses
36
+
37
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
38
+
39
+ ## Noncommercial Organizations
40
+
41
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
42
+
43
+ ## Fair Use
44
+
45
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
46
+
47
+ ## No Other Rights
48
+
49
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
50
+
51
+ ## Patent Defense
52
+
53
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
54
+
55
+ ## Violations
56
+
57
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
58
+
59
+ ## No Liability
60
+
61
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
62
+
63
+ ## Definitions
64
+
65
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
66
+
67
+ **You** refers to the individual or entity agreeing to these terms.
68
+
69
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
70
+
71
+ **Your licenses** are all the licenses granted to you for the software under these terms.
72
+
73
+ **Use** means anything you do with the software requiring one of your licenses.
74
+
75
+ Required Notice: Copyright 2026 Sitefig (https://sitefig.eu)
@@ -0,0 +1,46 @@
1
+ # susbot
2
+
3
+ The command-line version of [sus.bot](https://sus.bot/), a robots.txt checker. It reads a site's robots.txt and reports which search engines and AI crawlers may visit which pages, what is wrong with the file, and which paths it reveals to anyone who reads it. The same engine runs the website, so results match.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ cargo install susbot
9
+ ```
10
+
11
+ This installs a program called `susbot`. `cargo install susbot-cli` installs the same program.
12
+
13
+ ## Use
14
+
15
+ ```
16
+ susbot https://example.com # summary with issues, security findings and recommended actions
17
+ susbot https://example.com --format markdown --out audit.md
18
+ susbot https://example.com --format json # the full report, see the schema in the repository
19
+ susbot robots.txt --site-url https://example.com # a local file
20
+ susbot https://example.com --fail-on warning # exit 1 in CI when there are warnings or errors
21
+ susbot https://example.com --fail-on-security high # exit 1 when a high-risk path is exposed
22
+ susbot https://example.com --access-check # refetch with every crawler's user-agent
23
+ susbot --print-default-config > my-rules.toml # every rule, to adjust and pass with --config
24
+ ```
25
+
26
+ Other commands:
27
+
28
+ ```
29
+ susbot diff old.txt new.txt # what changed for crawlers, new sensitive paths, issues, sitemaps
30
+ susbot track --config sites.json --data-dir data # keep snapshots of many sites and report changes
31
+ susbot crawl --input domains.csv --out audit.jsonl.gz # audit a list of domains into a dataset
32
+ ```
33
+
34
+ Exit codes: 0 when all is well, 1 when findings reach `--fail-on` or `--fail-on-security`, 2 on fetch or configuration errors.
35
+
36
+ Reports are in English by default. For another EU language, pass `--lang` with `--locale-dir` pointing at the `locales` folder of the [repository](https://github.com/sitefig/robots).
37
+
38
+ ## More
39
+
40
+ - Website: [sus.bot](https://sus.bot/)
41
+ - GitHub Action and source: [github.com/sitefig/robots](https://github.com/sitefig/robots)
42
+ - The engine on its own: [susbot-core](https://crates.io/crates/susbot-core)
43
+
44
+ ## Licence
45
+
46
+ [PolyForm Noncommercial 1.0.0](LICENSE.md): free for noncommercial use. Commercial use, including running it in a company's CI, needs a licence from [Sitefig](https://sitefig.eu/).
@@ -0,0 +1,234 @@
1
+ //! `susbot audit`: one file or URL, every check, any export.
2
+
3
+ use crate::net::{fetch_robots, iso_now};
4
+ use clap::{Args, ValueEnum};
5
+ use std::io::Read;
6
+ use std::path::PathBuf;
7
+ use susbot_core::model::Level;
8
+ use susbot_core::{Analysis, Options};
9
+
10
+ #[derive(Clone, Copy, ValueEnum, Debug)]
11
+ pub enum Format {
12
+ /// Machine-readable report (schema/report.schema.json)
13
+ Json,
14
+ /// Client audit in Markdown
15
+ Markdown,
16
+ /// The audit as a standalone HTML document
17
+ Html,
18
+ /// All spreadsheet tabs as one tagged CSV
19
+ Csv,
20
+ /// Recommended actions and issue counts, for logs
21
+ Summary,
22
+ }
23
+
24
+ #[derive(Clone, Copy, ValueEnum, Debug, PartialEq)]
25
+ pub enum FailOn {
26
+ Never,
27
+ Error,
28
+ Warning,
29
+ }
30
+
31
+ #[derive(Clone, Copy, ValueEnum, Debug, PartialEq)]
32
+ pub enum Severity {
33
+ Never,
34
+ High,
35
+ Medium,
36
+ Low,
37
+ }
38
+
39
+ #[derive(Args, Debug)]
40
+ pub struct AuditArgs {
41
+ /// Site URL (https://example.com), a path to a robots.txt file, or "-" for stdin
42
+ #[arg(required_unless_present = "print_default_config", default_value = "")]
43
+ pub target: String,
44
+ /// TOML config merged over the built-in rules (see config/default.toml)
45
+ #[arg(short, long)]
46
+ pub config: Option<PathBuf>,
47
+ /// Print the built-in configuration and exit
48
+ #[arg(long)]
49
+ pub print_default_config: bool,
50
+ /// Output format
51
+ #[arg(short, long, value_enum, default_value = "summary")]
52
+ pub format: Format,
53
+ /// Write the output to a file instead of stdout
54
+ #[arg(short, long)]
55
+ pub out: Option<PathBuf>,
56
+ /// Language code for messages; loads <lang>.json from --locale-dir, ./locales or next to the binary
57
+ #[arg(long, default_value = "en")]
58
+ pub lang: String,
59
+ /// Directory holding <lang>.json dictionaries
60
+ #[arg(long)]
61
+ pub locale_dir: Option<PathBuf>,
62
+ /// Site URL assumed for a local file, enabling origin-dependent checks
63
+ #[arg(long)]
64
+ pub site_url: Option<String>,
65
+ /// Exit 1 when an issue at this level or worse is present
66
+ #[arg(long, value_enum, default_value = "never")]
67
+ pub fail_on: FailOn,
68
+ /// Exit 1 when a security finding at this severity or worse is present
69
+ #[arg(long, value_enum, default_value = "never")]
70
+ pub fail_on_security: Severity,
71
+ /// Also refetch the file with every crawler's real User-Agent and report differences
72
+ #[arg(long)]
73
+ pub access_check: bool,
74
+ /// Request timeout in seconds
75
+ #[arg(long, default_value = "15")]
76
+ pub timeout: u64,
77
+ }
78
+
79
+ pub fn load_locale(lang: &str, locale_dir: Option<&PathBuf>) -> Option<String> {
80
+ if lang == "en" {
81
+ return None;
82
+ }
83
+ let mut candidates = Vec::new();
84
+ if let Some(d) = locale_dir {
85
+ candidates.push(d.join(format!("{lang}.json")));
86
+ }
87
+ candidates.push(PathBuf::from("locales").join(format!("{lang}.json")));
88
+ if let Ok(exe) = std::env::current_exe() {
89
+ if let Some(dir) = exe.parent() {
90
+ candidates.push(dir.join("locales").join(format!("{lang}.json")));
91
+ }
92
+ }
93
+ let found = candidates.iter().find_map(|p| std::fs::read_to_string(p).ok());
94
+ if found.is_none() {
95
+ eprintln!("susbot: no dictionary for \"{lang}\" found (looked in {}); messages fall back to English", candidates.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", "));
96
+ }
97
+ found
98
+ }
99
+
100
+ fn access_check(origin: &str, analysis: &Analysis, timeout: u64) -> String {
101
+ let crawlers = &analysis.engine.config.crawlers;
102
+ let robots_url = format!("{origin}/robots.txt");
103
+ let client = ureq::AgentBuilder::new().timeout(std::time::Duration::from_secs(timeout)).build();
104
+ let get = |ua: &str| -> (u16, String) {
105
+ match client.get(&robots_url).set("User-Agent", ua).call() {
106
+ Ok(r) => {
107
+ let s = r.status();
108
+ (s, r.into_string().unwrap_or_default())
109
+ }
110
+ Err(ureq::Error::Status(s, r)) => (s, r.into_string().unwrap_or_default()),
111
+ Err(e) => (0, e.to_string()),
112
+ }
113
+ };
114
+ let (base_status, base_body) = get(&crawlers.browser_ua);
115
+ let mut lines = vec![format!("{:<28} {:>6} {}", "user-agent", "status", "body vs browser"), format!("{:<28} {:>6} -", "browser (baseline)", base_status)];
116
+ for c in crawlers.list.iter().filter(|c| c.ua.is_some()) {
117
+ let (s, body) = get(c.ua.as_deref().unwrap());
118
+ let diff = if s != base_status {
119
+ "status differs"
120
+ } else if body != base_body {
121
+ "body differs"
122
+ } else {
123
+ "same"
124
+ };
125
+ lines.push(format!("{:<28} {:>6} {diff}", c.name, s));
126
+ }
127
+ lines.join("\n")
128
+ }
129
+
130
+ pub fn run(cli: &AuditArgs) -> Result<u8, String> {
131
+ if cli.print_default_config {
132
+ print!("{}", susbot_core::config::DEFAULT_TOML);
133
+ return Ok(0);
134
+ }
135
+ if cli.target.is_empty() {
136
+ return Err("a target is required: a URL, a file, or - for stdin".into());
137
+ }
138
+ let config = match &cli.config {
139
+ Some(p) => Some(std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display()))?),
140
+ None => None,
141
+ };
142
+ let locale = load_locale(&cli.lang, cli.locale_dir.as_ref());
143
+ let is_url = cli.target.starts_with("http://") || cli.target.starts_with("https://") || (!cli.target.contains('/') && cli.target.contains('.') && !std::path::Path::new(&cli.target).exists());
144
+ let (text, fetch, site_url, origin) = if cli.target == "-" {
145
+ let mut s = String::new();
146
+ std::io::stdin().read_to_string(&mut s).map_err(|e| e.to_string())?;
147
+ (s, None, cli.site_url.clone(), None)
148
+ } else if is_url {
149
+ let raw = if cli.target.contains("://") { cli.target.clone() } else { format!("https://{}", cli.target) };
150
+ let u = url::Url::parse(&raw).map_err(|e| format!("{raw}: {e}"))?;
151
+ let origin = u.origin().ascii_serialization();
152
+ let engine = susbot_core::Engine::from_toml(config.as_deref())?;
153
+ let f = fetch_robots(&origin, &engine.config.crawlers.browser_ua, cli.timeout)?;
154
+ let text = f.text.clone();
155
+ let site = f.final_url.clone();
156
+ (text, Some(f), Some(site), Some(origin))
157
+ } else {
158
+ let s = std::fs::read_to_string(&cli.target).map_err(|e| format!("{}: {e}", cli.target))?;
159
+ (s, None, cli.site_url.clone(), None)
160
+ };
161
+
162
+ let analysis = Analysis::new(&text, Options { site_url, fetch, config, lang: Some(cli.lang.clone()), locale, now: Some(iso_now()), schema_url: Some("https://sus.bot/schema/report.schema.json".into()) })?;
163
+ let r = &analysis.report;
164
+
165
+ let mut output = match cli.format {
166
+ Format::Json => analysis.report_json_pretty() + "\n",
167
+ Format::Markdown => analysis.markdown(),
168
+ Format::Html => analysis.html(),
169
+ Format::Csv => analysis.tagged_csv(),
170
+ Format::Summary => {
171
+ let mut s = format!(
172
+ "{}\n errors {}, warnings {}, notes {}; security findings {}; AI training crawlers blocked {}/{}\n\n",
173
+ r.source.final_url.clone().or(r.source.site_url.clone()).unwrap_or_else(|| cli.target.clone()),
174
+ r.summary.issues.errors,
175
+ r.summary.issues.warnings,
176
+ r.summary.issues.notes,
177
+ r.summary.security_findings,
178
+ r.summary.ai_training.blocked,
179
+ r.summary.ai_training.total
180
+ );
181
+ let mut issues: Vec<_> = r.issues.iter().filter(|i| i.level != Level::Info).collect();
182
+ issues.sort_by(|a, b| a.level.cmp(&b.level).then(a.line.unwrap_or(u32::MAX).cmp(&b.line.unwrap_or(u32::MAX))));
183
+ for i in issues {
184
+ s.push_str(&format!("{:<7} {:>5} {}\n", i.level.as_str(), i.line.map(|l| format!("L{l}")).unwrap_or_default(), i.message));
185
+ }
186
+ for f in &r.security {
187
+ s.push_str(&format!("{:<7} {:>5} Disallow: {} ({}, {})\n", f.severity, format!("L{}", f.line), f.path, f.category, f.reason));
188
+ }
189
+ s.push_str("\nRecommended actions:\n");
190
+ for (i, a) in analysis.recommended_actions().iter().enumerate() {
191
+ s.push_str(&format!("{}. {a}\n", i + 1));
192
+ }
193
+ s
194
+ }
195
+ };
196
+ if cli.access_check {
197
+ if let Some(origin) = &origin {
198
+ let table = access_check(origin, &analysis, cli.timeout);
199
+ if matches!(cli.format, Format::Summary | Format::Markdown) {
200
+ output.push_str("\nReal access by user-agent:\n");
201
+ output.push_str(&table);
202
+ output.push('\n');
203
+ } else {
204
+ eprintln!("{table}");
205
+ }
206
+ } else {
207
+ eprintln!("susbot: --access-check needs a URL target");
208
+ }
209
+ }
210
+ match &cli.out {
211
+ Some(p) => std::fs::write(p, output).map_err(|e| format!("{}: {e}", p.display()))?,
212
+ None => print!("{output}"),
213
+ }
214
+
215
+ let worst_issue = r.issues.iter().map(|i| i.level).min();
216
+ let fail_issue = match cli.fail_on {
217
+ FailOn::Never => false,
218
+ FailOn::Error => worst_issue == Some(Level::Error),
219
+ FailOn::Warning => matches!(worst_issue, Some(Level::Error) | Some(Level::Warning)),
220
+ };
221
+ let rank = |s: &str| match s {
222
+ "high" => 0,
223
+ "medium" => 1,
224
+ _ => 2,
225
+ };
226
+ let worst_sec = r.security.iter().map(|f| rank(&f.severity)).min();
227
+ let fail_sec = match cli.fail_on_security {
228
+ Severity::Never => false,
229
+ Severity::High => worst_sec == Some(0),
230
+ Severity::Medium => matches!(worst_sec, Some(0) | Some(1)),
231
+ Severity::Low => worst_sec.is_some(),
232
+ };
233
+ Ok(if fail_issue || fail_sec { 1 } else { 0 })
234
+ }