diff --git a/README.md b/README.md index 9999ce9d..0f9e74d9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@
+> **This is `ghunt-fixed`, a fork of [mxrch/GHunt](https://github.com/mxrch/GHunt), modified on 2026-07-31.** +> The upstream PyPI release (2.3.4) crashes with `KeyError: 'container'` on some accounts because +> Google's People API responses now sometimes omit `metadata.container`. This fork patches +> `ghunt/parsers/people.py` to handle that missing key gracefully instead of crashing +> (see [upstream PR #593](https://github.com/mxrch/GHunt/pull/593), not yet merged/released as of this fork). +> Still licensed under AGPLv3, same as upstream — see [LICENSE.md](LICENSE.md). + +
+ #### 🌐 GHunt Online version : https://osint.industries #### 🐍 Now Python 3.13 compatible ! diff --git a/ghunt/parsers/people.py b/ghunt/parsers/people.py index 43554121..99143bb0 100644 --- a/ghunt/parsers/people.py +++ b/ghunt/parsers/people.py @@ -60,7 +60,9 @@ async def _scrape(self, as_client: httpx.AsyncClient, photo_data: Dict[str, any] self.isDefault, self.flathash = await is_default_profile_pic(as_client, self.url) elif photo_type == "cover_photo": - self.url = '='.join(photo_data.get("imageUrl").split("=")[:-1]) + image_url = photo_data.get("imageUrl", "") + if image_url: + self.url = '='.join(image_url.split("=")[:-1]) if (isDefault := photo_data.get("isDefault")): self.isDefault = isDefault else: @@ -107,8 +109,8 @@ def __init__(self): def _scrape(self, apps_data, container_name: str): for app in apps_data: - if app["metadata"]["container"] == container_name: - self.apps.append(app["appType"].title()) + if app.get("metadata", {}).get("container") == container_name: + self.apps.append(app.get("appType", "").title()) class PersonContainers(dict): pass @@ -129,45 +131,61 @@ async def _scrape(self, as_client: httpx.AsyncClient, person_data: Dict[str, any self.personId = person_data.get("personId") if person_data.get("email"): for email_data in person_data["email"]: + if not (container := email_data.get("metadata", {}).get("container")): + continue person_email = PersonEmail() person_email._scrape(email_data) - self.emails[email_data["metadata"]["container"]] = person_email + self.emails[container] = person_email if person_data.get("name"): for name_data in person_data["name"]: + if not (container := name_data.get("metadata", {}).get("container")): + continue person_name = PersonName() person_name._scrape(name_data) - self.names[name_data["metadata"]["container"]] = person_name + self.names[container] = person_name if person_data.get("readOnlyProfileInfo"): for profile_data in person_data["readOnlyProfileInfo"]: + if not (container := profile_data.get("metadata", {}).get("container")): + continue person_profile = PersonProfileInfo() person_profile._scrape(profile_data) - self.profileInfos[profile_data["metadata"]["container"]] = person_profile - - if person_data.get("photo"): - for photo_data in person_data["photo"]: - person_photo = PersonPhoto() - await person_photo._scrape(as_client, photo_data, "profile_photo") - self.profilePhotos[profile_data["metadata"]["container"]] = person_photo + self.profileInfos[container] = person_profile + + # Each photo carries its own container in its metadata, independent of + # readOnlyProfileInfo's containers, so it's keyed separately rather than + # nested in the loop above (that previously mis-attributed every photo + # to whichever profile container happened to be current in the outer loop). + if person_data.get("photo"): + for photo_data in person_data["photo"]: + if not (container := photo_data.get("metadata", {}).get("container")): + continue + person_photo = PersonPhoto() + await person_photo._scrape(as_client, photo_data, "profile_photo") + self.profilePhotos[container] = person_photo if (source_ids := person_data.get("metadata", {}).get("identityInfo", {}).get("sourceIds")): for source_ids_data in source_ids: + if not (container := source_ids_data.get("container")): + continue person_source_ids = PersonSourceIds() person_source_ids._scrape(source_ids_data) - self.sourceIds[source_ids_data["container"]] = person_source_ids + self.sourceIds[container] = person_source_ids if person_data.get("coverPhoto"): for cover_photo_data in person_data["coverPhoto"]: + if not (container := cover_photo_data.get("metadata", {}).get("container")): + continue person_cover_photo = PersonPhoto() await person_cover_photo._scrape(as_client, cover_photo_data, "cover_photo") - container = cover_photo_data.get("metadata", {}).get("container", "unknown") self.coverPhotos[container] = person_cover_photo if (apps_data := person_data.get("inAppReachability")): containers_names = set() for app_data in person_data["inAppReachability"]: - containers_names.add(app_data["metadata"]["container"]) + if (container := app_data.get("metadata", {}).get("container")): + containers_names.add(container) for container_name in containers_names: person_app_reachability = PersonInAppReachability()