{"id":48932743,"url":"https://github.com/RyanDFIR/unfurl","last_synced_at":"2026-04-22T03:00:44.736Z","repository":{"id":38428735,"uuid":"219613650","full_name":"RyanDFIR/unfurl","owner":"RyanDFIR","description":"Extract and Visualize Data from URLs using Unfurl","archived":false,"fork":false,"pushed_at":"2026-04-16T00:03:02.000Z","size":23365,"stargazers_count":736,"open_issues_count":40,"forks_count":63,"subscribers_count":21,"default_branch":"main","last_synced_at":"2026-04-16T12:25:48.954Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://unfurl.link","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RyanDFIR.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"docs/contributing.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-11-04T23:06:57.000Z","updated_at":"2026-04-16T00:03:04.000Z","dependencies_parsed_at":"2022-07-16T09:46:26.789Z","dependency_job_id":"9fbd9829-64bf-4b5e-a55c-79115a7b88c3","html_url":"https://github.com/RyanDFIR/unfurl","commit_stats":{"total_commits":307,"total_committers":13,"mean_commits":"23.615384615384617","dds":"0.19543973941368076","last_synced_commit":"4793e004762e53fd1f4f252ddbff414e16e2f293"},"previous_names":["ryandfir/unfurl","obsidianforensics/unfurl"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/RyanDFIR/unfurl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RyanDFIR%2Funfurl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RyanDFIR%2Funfurl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RyanDFIR%2Funfurl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RyanDFIR%2Funfurl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RyanDFIR","download_url":"https://codeload.github.com/RyanDFIR/unfurl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RyanDFIR%2Funfurl/sbom","scorecard":{"id":701232,"data":{"date":"2025-08-11","repo":{"name":"github.com/obsidianforensics/unfurl","commit":"f0d0a7c350d482bcd5de75fa542c7189126cf71a"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.8,"checks":[{"name":"Maintained","score":10,"reason":"13 commit(s) and 5 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/13 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/publish-to-pypi.yml:1","Warn: no topLevel permission defined: .github/workflows/publish-to-test-pypi.yml:1","Warn: no topLevel permission defined: .github/workflows/unit-tests.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/publish-to-pypi.yml:8"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish-to-pypi.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/publish-to-pypi.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish-to-pypi.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/publish-to-pypi.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/publish-to-pypi.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/publish-to-pypi.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish-to-test-pypi.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/publish-to-test-pypi.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish-to-test-pypi.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/publish-to-test-pypi.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/publish-to-test-pypi.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/publish-to-test-pypi.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/unit-tests.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/unit-tests.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/unit-tests.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/obsidianforensics/unfurl/unit-tests.yml/main?enable=pin","Warn: containerImage not pinned by hash: Dockerfile:1: pin your Docker image by updating python:3.11-alpine to python:3.11-alpine@sha256:8d8c6d3808243160605925c2a7ab2dc5c72d0e75651699b0639143613e0855b8","Warn: pipCommand not pinned by hash: Dockerfile:9-12","Warn: pipCommand not pinned by hash: Dockerfile:9-12","Warn: pipCommand not pinned by hash: Dockerfile:9-12","Warn: pipCommand not pinned by hash: Dockerfile:16","Warn: pipCommand not pinned by hash: .github/workflows/publish-to-pypi.yml:24","Warn: pipCommand not pinned by hash: .github/workflows/publish-to-test-pypi.yml:23","Warn: pipCommand not pinned by hash: .github/workflows/unit-tests.yml:29","Warn: pipCommand not pinned by hash: .github/workflows/unit-tests.yml:30","Warn: pipCommand not pinned by hash: .github/workflows/unit-tests.yml:31","Warn: pipCommand not pinned by hash: .github/workflows/unit-tests.yml:32","Info:   0 out of   6 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 third-party GitHubAction dependencies pinned","Info:   0 out of  10 pipCommand dependencies pinned","Info:   0 out of   1 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Vulnerabilities","score":0,"reason":"15 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-43qf-4rqw-9q2g","Warn: Project is vulnerable to: GHSA-7rxf-gvfg-47g4","Warn: Project is vulnerable to: GHSA-84pr-m4jr-85g5","Warn: Project is vulnerable to: GHSA-8vgw-p6qm-5gr7","Warn: Project is vulnerable to: PYSEC-2024-71 / GHSA-hxwh-jpp2-84pm","Warn: Project is vulnerable to: PYSEC-2020-43 / GHSA-xc3p-ff3m-f46v","Warn: Project is vulnerable to: GHSA-2g68-c3qc-8985","Warn: Project is vulnerable to: GHSA-f9vj-2wh5-fj8j","Warn: Project is vulnerable to: PYSEC-2023-221 / GHSA-hrfv-mqp8-q5rw","Warn: Project is vulnerable to: GHSA-q34m-jh98-gwm2","Warn: Project is vulnerable to: PYSEC-2022-48 / GHSA-77rm-9x9h-xj3g","Warn: Project is vulnerable to: GHSA-8gq9-2x98-w8hf","Warn: Project is vulnerable to: GHSA-8qvm-5x2c-j2w7","Warn: Project is vulnerable to: PYSEC-2017-65 / GHSA-jwvw-v7c5-m82h","Warn: Project is vulnerable to: GHSA-9hjg-9r4m-mvj7"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 28 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-22T05:15:24.331Z","repository_id":38428735,"created_at":"2025-08-22T05:15:24.332Z","updated_at":"2025-08-22T05:15:24.332Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32119065,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-22T00:31:26.853Z","status":"online","status_checked_at":"2026-04-22T02:00:05.693Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-17T10:00:45.985Z","updated_at":"2026-04-22T03:00:44.725Z","avatar_url":"https://github.com/RyanDFIR.png","language":"Python","funding_links":[],"categories":["Python","Tools"],"sub_categories":["Internet Artifacts"],"readme":"\u003cpicture\u003e\n  \u003csource srcset=\"/unfurl/static/unfurl_dark.png\" media=\"(prefers-color-scheme: dark)\"\u003e\n  \u003cimg src=\"/unfurl/static/unfurl.png\" alt=\"Unfurl Logo\"\u003e\n\u003c/picture\u003e\n\n# Extract and Visualize Data from URLs using Unfurl\nUnfurl takes a URL and expands (\"unfurls\") it into a directed graph, extracting every bit of information from the URL and \nexposing the obscured. It does this by breaking up a URL into components, extracting as much information as it can from \neach piece, and presenting it all visually. This “show your work” approach (along with embedded references and documentation) \nmakes the analysis transparent to the user and helps them learn about (and discover) semantic and syntactical URL structures.\n\nUnfurl has parsers for URLs, search engines, chat applications, social media sites, and more. It also has more generic parsers \n(timestamps, UUIDs, etc) helpful for exploring new URLs or reverse engineering. It’s also easy to build new parsers, since \nUnfurl is open source (Python 3) and has an extensible plugin system.\n\nNo matter if you extracted a URL from a memory image, carved it from slack space, or pulled it from a browser’s history file, \nUnfurl can help you get the most out of it.\n\n\u003cimg src=\"docs/unfurl-demo.gif\"/\u003e\n\n## How to use Unfurl\n\n### Online Version\n\n1. There is an online version at **https://dfir.blog/unfurl**. Visit that page, enter the URL in the form, and \nclick 'Unfurl!'. \n2. You can also access the online version using a bookmarklet - create a new bookmark and paste \n`javascript:window.location.href='https://dfir.blog/unfurl/?url='+window.location.href;` as the location. Then when on any\npage with an interesting URL, you can click the bookmarklet and see the URL \"unfurled\".\n\n### Local Python Install\n\n1. Install via pip: `pip install dfir-unfurl[all]`\n\nAfter Unfurl is installed, you can run use it via the web app or command-line:\n\n1. Run `unfurl_app`\n1. Browse to localhost:5000/ (editable via config file)\n1. Enter the URL to unfurl in the form, and 'Unfurl!'\n\nOR\n\n1. Run `unfurl https://twitter.com/_RyanBenson/status/1205161015177961473`\n1. Output: \n```\n[1] https://twitter.com/_RyanBenson/status/1205161015177961473\n ├─(u)─[2] Scheme: https\n ├─(u)─[3] twitter.com\n |  ├─(u)─[5] Domain Name: twitter.com\n |  └─(u)─[6] TLD: com\n └─(u)─[4] /_RyanBenson/status/1205161015177961473\n    ├─(u)─[7] 1: _RyanBenson\n    ├─(u)─[8] 2: status\n    └─(u)─[9] 3: 1205161015177961473\n       ├─(❄)─[10] Timestamp: 1576167751484\n       |  └─(🕓)─[13] 2019-12-12 16:22:31.484\n       ├─(❄)─[11] Machine ID: 334\n       └─(❄)─[12] Sequence: 1 \n```\n\nIf the URL has special characters (like \"\u0026\") that your shell might interpret as a command, put the URL in quotes. \nExample: `unfurl \"https://www.google.com/search?\u0026ei=yTLGXeyKN_2y0PEP2smVuAg\u0026q=dfir.blog\u0026oq=dfir.blog\u0026ved=0ahUKEwisk-WjmNzlAhV9GTQIHdpkBYcQ4dUDCAg\"`\n\n`unfurl` has a number of command line options to modify its behavior:\n```\noptional arguments:\n  -h, --help            show this help message and exit\n  -d, --detailed        show more detailed explanations.\n  -f FILTER, --filter FILTER\n                        only output lines that match this filter.\n  -o OUTPUT, --output OUTPUT\n                        file to save output (as CSV) to. if omitted, output is sent to stdout (typically this means displayed in the console).\n  -v, -V, --version     show program's version number and exit\n```\n\n### Docker \n\n1. `git clone https://github.com/obsidianforensics/unfurl`\n1. `cd unfurl`\n1. `docker-compose up -d`\n\n## Testing \n\n1. All tests are run automatically on each PR by Travis CI. Tests need to pass before merging. \n1. While not required, it is strongly encouraged to add tests that cover any new features in a PR. \n1. To manually run all tests (units and integration): ``python -m unittest discover -s unfurl/tests``\n\nIf using Docker as above, run: \n``docker exec unfurl python -m unittest discover -s unfurl/tests``\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FRyanDFIR%2Funfurl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FRyanDFIR%2Funfurl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FRyanDFIR%2Funfurl/lists"}